commit a234e55e64c951abb43a81af00d5488a40ab04a4 Author: root Date: Tue May 26 08:07:45 2026 +0000 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..39f48d9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*] +charset = utf-8 + +# Tab indentation (no size specified) +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62136cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +.DS_Store + +application/cache/* +!application/cache/index.html + +application/logs/* +!application/logs/index.html + +!application/*/.htaccess + +tests/mocks/database/ci_test.sqlite + +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* + +# IDE Files +#------------------------- +/nbproject/ +.idea/* + +## Sublime Text cache files +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project +/tests/tests/ +/tests/results/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..796d3b6 --- /dev/null +++ b/.htaccess @@ -0,0 +1,9 @@ + + RewriteEngine On + RewriteBase / + + # Redirect requests to index.php + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.*)$ index.php?/$1 [L] + diff --git a/application/.htaccess b/application/.htaccess new file mode 100644 index 0000000..6c63ed4 --- /dev/null +++ b/application/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + \ No newline at end of file diff --git a/application/cache/index.html b/application/cache/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/cache/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/config/autoload.php b/application/config/autoload.php new file mode 100644 index 0000000..e43f177 --- /dev/null +++ b/application/config/autoload.php @@ -0,0 +1,135 @@ + 'ua'); +*/ +$autoload['libraries'] = array('session', 'database'); + +/* +| ------------------------------------------------------------------- +| Auto-load Drivers +| ------------------------------------------------------------------- +| These classes are located in system/libraries/ or in your +| application/libraries/ directory, but are also placed inside their +| own subdirectory and they extend the CI_Driver_Library class. They +| offer multiple interchangeable driver options. +| +| Prototype: +| +| $autoload['drivers'] = array('cache'); +| +| You can also supply an alternative property name to be assigned in +| the controller: +| +| $autoload['drivers'] = array('cache' => 'cch'); +| +*/ +$autoload['drivers'] = array(); + +/* +| ------------------------------------------------------------------- +| Auto-load Helper Files +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['helper'] = array('url', 'file'); +*/ +$autoload['helper'] = array('url', 'file', 'permission', 'activity', 'indo_date'); + +/* +| ------------------------------------------------------------------- +| Auto-load Config files +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['config'] = array('config1', 'config2'); +| +| NOTE: This item is intended for use ONLY if you have created custom +| config files. Otherwise, leave it blank. +| +*/ +$autoload['config'] = array(); + +/* +| ------------------------------------------------------------------- +| Auto-load Language files +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['language'] = array('lang1', 'lang2'); +| +| NOTE: Do not include the "_lang" part of your file. For example +| "codeigniter_lang.php" would be referenced as array('codeigniter'); +| +*/ +$autoload['language'] = array(); + +/* +| ------------------------------------------------------------------- +| Auto-load Models +| ------------------------------------------------------------------- +| Prototype: +| +| $autoload['model'] = array('first_model', 'second_model'); +| +| You can also supply an alternative model name to be assigned +| in the controller: +| +| $autoload['model'] = array('first_model' => 'first'); +*/ +$autoload['model'] = array(); diff --git a/application/config/config.php b/application/config/config.php new file mode 100644 index 0000000..93230af --- /dev/null +++ b/application/config/config.php @@ -0,0 +1,533 @@ +]+$/i +| +| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! +| +*/ +$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-'; + +/* +|-------------------------------------------------------------------------- +| Enable Query Strings +|-------------------------------------------------------------------------- +| +| By default CodeIgniter uses search-engine friendly segment based URLs: +| example.com/who/what/where/ +| +| You can optionally enable standard query string based URLs: +| example.com?who=me&what=something&where=here +| +| Options are: TRUE or FALSE (boolean) +| +| The other items let you set the query string 'words' that will +| invoke your controllers and its functions: +| example.com/index.php?c=controller&m=function +| +| Please note that some of the helpers won't work as expected when +| this feature is enabled, since CodeIgniter is designed primarily to +| use segment based URLs. +| +*/ +$config['enable_query_strings'] = FALSE; +$config['controller_trigger'] = 'c'; +$config['function_trigger'] = 'm'; +$config['directory_trigger'] = 'd'; + +/* +|-------------------------------------------------------------------------- +| Allow $_GET array +|-------------------------------------------------------------------------- +| +| By default CodeIgniter enables access to the $_GET array. If for some +| reason you would like to disable it, set 'allow_get_array' to FALSE. +| +| WARNING: This feature is DEPRECATED and currently available only +| for backwards compatibility purposes! +| +*/ +$config['allow_get_array'] = TRUE; + +/* +|-------------------------------------------------------------------------- +| Error Logging Threshold +|-------------------------------------------------------------------------- +| +| You can enable error logging by setting a threshold over zero. The +| threshold determines what gets logged. Threshold options are: +| +| 0 = Disables logging, Error logging TURNED OFF +| 1 = Error Messages (including PHP errors) +| 2 = Debug Messages +| 3 = Informational Messages +| 4 = All Messages +| +| You can also pass an array with threshold levels to show individual error types +| +| array(2) = Debug Messages, without Error Messages +| +| For a live site you'll usually only enable Errors (1) to be logged otherwise +| your log files will fill up very fast. +| +*/ +$config['log_threshold'] = 4; + +/* +|-------------------------------------------------------------------------- +| Error Logging Directory Path +|-------------------------------------------------------------------------- +| +| Leave this BLANK unless you would like to set something other than the default +| application/logs/ directory. Use a full server path with trailing slash. +| +*/ +$config['log_path'] = ''; + +/* +|-------------------------------------------------------------------------- +| Log File Extension +|-------------------------------------------------------------------------- +| +| The default filename extension for log files. The default 'php' allows for +| protecting the log files via basic scripting, when they are to be stored +| under a publicly accessible directory. +| +| Note: Leaving it blank will default to 'php'. +| +*/ +$config['log_file_extension'] = ''; + +/* +|-------------------------------------------------------------------------- +| Log File Permissions +|-------------------------------------------------------------------------- +| +| The file system permissions to be applied on newly created log files. +| +| IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal +| integer notation (i.e. 0700, 0644, etc.) +*/ +$config['log_file_permissions'] = 0644; + +/* +|-------------------------------------------------------------------------- +| Date Format for Logs +|-------------------------------------------------------------------------- +| +| Each item that is logged has an associated date. You can use PHP date +| codes to set your own date formatting +| +*/ +$config['log_date_format'] = 'Y-m-d H:i:s'; + +/* +|-------------------------------------------------------------------------- +| Error Views Directory Path +|-------------------------------------------------------------------------- +| +| Leave this BLANK unless you would like to set something other than the default +| application/views/errors/ directory. Use a full server path with trailing slash. +| +*/ +$config['error_views_path'] = ''; + +/* +|-------------------------------------------------------------------------- +| Cache Directory Path +|-------------------------------------------------------------------------- +| +| Leave this BLANK unless you would like to set something other than the default +| application/cache/ directory. Use a full server path with trailing slash. +| +*/ +$config['cache_path'] = ''; + +/* +|-------------------------------------------------------------------------- +| Cache Include Query String +|-------------------------------------------------------------------------- +| +| Whether to take the URL query string into consideration when generating +| output cache files. Valid options are: +| +| FALSE = Disabled +| TRUE = Enabled, take all query parameters into account. +| Please be aware that this may result in numerous cache +| files generated for the same page over and over again. +| array('q') = Enabled, but only take into account the specified list +| of query parameters. +| +*/ +$config['cache_query_string'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Encryption Key +|-------------------------------------------------------------------------- +| +| If you use the Encryption class, you must set an encryption key. +| See the user guide for more info. +| +| https://codeigniter.com/userguide3/libraries/encryption.html +| +*/ +$config['encryption_key'] = ''; + +/* +|-------------------------------------------------------------------------- +| Session Variables +|-------------------------------------------------------------------------- +| +| 'sess_driver' +| +| The storage driver to use: files, database, redis, memcached +| +| 'sess_cookie_name' +| +| The session cookie name, must contain only [0-9a-z_-] characters +| +| 'sess_samesite' +| +| Session cookie SameSite attribute: Lax (default), Strict or None +| +| 'sess_expiration' +| +| The number of SECONDS you want the session to last. +| Setting to 0 (zero) means expire when the browser is closed. +| +| 'sess_save_path' +| +| The location to save sessions to, driver dependent. +| +| For the 'files' driver, it's a path to a writable directory. +| WARNING: Only absolute paths are supported! +| +| For the 'database' driver, it's a table name. +| Please read up the manual for the format with other session drivers. +| +| IMPORTANT: You are REQUIRED to set a valid save path! +| +| 'sess_match_ip' +| +| Whether to match the user's IP address when reading the session data. +| +| WARNING: If you're using the database driver, don't forget to update +| your session table's PRIMARY KEY when changing this setting. +| +| 'sess_time_to_update' +| +| How many seconds between CI regenerating the session ID. +| +| 'sess_regenerate_destroy' +| +| Whether to destroy session data associated with the old session ID +| when auto-regenerating the session ID. When set to FALSE, the data +| will be later deleted by the garbage collector. +| +| Other session cookie settings are shared with the rest of the application, +| except for 'cookie_prefix' and 'cookie_httponly', which are ignored here. +| +*/ +$config['sess_driver'] = 'files'; +$config['sess_cookie_name'] = 'ci_session'; +$config['sess_samesite'] = 'Lax'; +$config['sess_expiration'] = 7200; +$config['sess_save_path'] = sys_get_temp_dir(); +$config['sess_match_ip'] = FALSE; +$config['sess_time_to_update'] = 300; +$config['sess_regenerate_destroy'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Cookie Related Variables +|-------------------------------------------------------------------------- +| +| 'cookie_prefix' = Set a cookie name prefix if you need to avoid collisions +| 'cookie_domain' = Set to .your-domain.com for site-wide cookies +| 'cookie_path' = Typically will be a forward slash +| 'cookie_secure' = Cookie will only be set if a secure HTTPS connection exists. +| 'cookie_httponly' = Cookie will only be accessible via HTTP(S) (no javascript) +| 'cookie_samesite' = Cookie's samesite attribute (Lax, Strict or None) +| +| Note: These settings (with the exception of 'cookie_prefix' and +| 'cookie_httponly') will also affect sessions. +| +*/ +$config['cookie_prefix'] = ''; +$config['cookie_domain'] = ''; +$config['cookie_path'] = '/'; +$config['cookie_secure'] = FALSE; +$config['cookie_httponly'] = FALSE; +$config['cookie_samesite'] = 'Lax'; + +/* +|-------------------------------------------------------------------------- +| Standardize newlines +|-------------------------------------------------------------------------- +| +| Determines whether to standardize newline characters in input data, +| meaning to replace \r\n, \r, \n occurrences with the PHP_EOL value. +| +| WARNING: This feature is DEPRECATED and currently available only +| for backwards compatibility purposes! +| +*/ +$config['standardize_newlines'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Global XSS Filtering +|-------------------------------------------------------------------------- +| +| Determines whether the XSS filter is always active when GET, POST or +| COOKIE data is encountered +| +| WARNING: This feature is DEPRECATED and currently available only +| for backwards compatibility purposes! +| +*/ +$config['global_xss_filtering'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Cross Site Request Forgery +|-------------------------------------------------------------------------- +| Enables a CSRF cookie token to be set. When set to TRUE, token will be +| checked on a submitted form. If you are accepting user data, it is strongly +| recommended CSRF protection be enabled. +| +| 'csrf_token_name' = The token name +| 'csrf_cookie_name' = The cookie name +| 'csrf_expire' = The number in seconds the token should expire. +| 'csrf_regenerate' = Regenerate token on every submission +| 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks +*/ +$config['csrf_protection'] = FALSE; +$config['csrf_token_name'] = 'csrf_test_name'; +$config['csrf_cookie_name'] = 'csrf_cookie_name'; +$config['csrf_expire'] = 7200; +$config['csrf_regenerate'] = TRUE; +$config['csrf_exclude_uris'] = array(); + +/* +|-------------------------------------------------------------------------- +| Output Compression +|-------------------------------------------------------------------------- +| +| Enables Gzip output compression for faster page loads. When enabled, +| the output class will test whether your server supports Gzip. +| Even if it does, however, not all browsers support compression +| so enable only if you are reasonably sure your visitors can handle it. +| +| Only used if zlib.output_compression is turned off in your php.ini. +| Please do not use it together with httpd-level output compression. +| +| VERY IMPORTANT: If you are getting a blank page when compression is enabled it +| means you are prematurely outputting something to your browser. It could +| even be a line of whitespace at the end of one of your scripts. For +| compression to work, nothing can be sent before the output buffer is called +| by the output class. Do not 'echo' any values with compression enabled. +| +*/ +$config['compress_output'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Master Time Reference +|-------------------------------------------------------------------------- +| +| Options are 'local' or any PHP supported timezone. This preference tells +| the system whether to use your server's local time as the master 'now' +| reference, or convert it to the configured one timezone. See the 'date +| helper' page of the user guide for information regarding date handling. +| +*/ +$config['time_reference'] = 'local'; + +/* +|-------------------------------------------------------------------------- +| Rewrite PHP Short Tags +|-------------------------------------------------------------------------- +| +| If your PHP installation does not have short tag support enabled CI +| can rewrite the tags on-the-fly, enabling you to utilize that syntax +| in your view files. Options are TRUE or FALSE (boolean) +| +| Note: You need to have eval() enabled for this to work. +| +*/ +$config['rewrite_short_tags'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| Reverse Proxy IPs +|-------------------------------------------------------------------------- +| +| If your server is behind a reverse proxy, you must whitelist the proxy +| IP addresses from which CodeIgniter should trust headers such as +| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify +| the visitor's IP address. +| +| You can use both an array or a comma-separated list of proxy addresses, +| as well as specifying whole subnets. Here are a few examples: +| +| Comma-separated: '10.0.1.200,192.168.5.0/24' +| Array: array('10.0.1.200', '192.168.5.0/24') +*/ +$config['proxy_ips'] = ''; diff --git a/application/config/constants.php b/application/config/constants.php new file mode 100644 index 0000000..18d3b4b --- /dev/null +++ b/application/config/constants.php @@ -0,0 +1,85 @@ +db->last_query() and profiling of DB queries. +| When you run a query, with this setting set to TRUE (default), +| CodeIgniter will store the SQL statement for debugging purposes. +| However, this may cause high memory usage, especially if you run +| a lot of SQL queries ... disable this to avoid that problem. +| +| The $active_group variable lets you choose which connection group to +| make active. By default there is only one group (the 'default' group). +| +| The $query_builder variables lets you determine whether or not to load +| the query builder class. +*/ +$active_group = 'default'; +$query_builder = TRUE; + +$db['default'] = array( + 'dsn' => '', + 'hostname' => '103.242.106.56', // tanpa port + 'port' => 3309, // port PostgreSQL + 'username' => 'keuangan', + 'password' => '@P4ssw0rd', + 'database' => 'keuangan', + 'dbdriver' => 'mysqli', // WAJIB: driver PostgreSQL + 'dbprefix' => '', + 'pconnect' => FALSE, + 'db_debug' => (ENVIRONMENT !== 'production'), + 'cache_on' => FALSE, + 'cachedir' => '', + 'char_set' => 'utf8', + 'dbcollat' => 'utf8_general_ci', // PostgreSQL abaikan ini, tapi biarkan saja + 'swap_pre' => '', + 'encrypt' => FALSE, + 'compress' => FALSE, + 'stricton' => FALSE, + 'failover' => array(), + 'save_queries' => TRUE +); + diff --git a/application/config/doctypes.php b/application/config/doctypes.php new file mode 100644 index 0000000..59a7991 --- /dev/null +++ b/application/config/doctypes.php @@ -0,0 +1,24 @@ + '', + 'xhtml1-strict' => '', + 'xhtml1-trans' => '', + 'xhtml1-frame' => '', + 'xhtml-basic11' => '', + 'html5' => '', + 'html4-strict' => '', + 'html4-trans' => '', + 'html4-frame' => '', + 'mathml1' => '', + 'mathml2' => '', + 'svg10' => '', + 'svg11' => '', + 'svg11-basic' => '', + 'svg11-tiny' => '', + 'xhtml-math-svg-xh' => '', + 'xhtml-math-svg-sh' => '', + 'xhtml-rdfa-1' => '', + 'xhtml-rdfa-2' => '' +); diff --git a/application/config/foreign_chars.php b/application/config/foreign_chars.php new file mode 100644 index 0000000..0231f35 --- /dev/null +++ b/application/config/foreign_chars.php @@ -0,0 +1,114 @@ + 'ae', + '/ö|œ/' => 'oe', + '/ü/' => 'ue', + '/Ä/' => 'Ae', + '/Ü/' => 'Ue', + '/Ö/' => 'Oe', + '/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', + '/ç|ć|ĉ|ċ|č/' => 'c', + '/Д|Δ/' => 'D', + '/д|δ/' => 'd', + '/Ð|Ď|Đ/' => 'Dj', + '/ð|ď|đ/' => 'dj', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e', + '/Ф/' => 'F', + '/ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G', + '/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g', + '/Ĥ|Ħ/' => 'H', + '/ĥ|ħ/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i', + '/Ĵ/' => 'J', + '/ĵ/' => 'j', + '/Θ/' => 'TH', + '/θ/' => 'th', + '/Ķ|Κ|К/' => 'K', + '/ķ|κ|к/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l', + '/М/' => 'M', + '/м/' => 'm', + '/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N', + '/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R', + '/ŕ|ŗ|ř|ρ|р/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S', + '/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's', + '/Ț|Ţ|Ť|Ŧ|Τ|Т/' => 'T', + '/ț|ţ|ť|ŧ|τ|т/' => 't', + '/Þ|þ/' => 'th', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u', + '/Ƴ|Ɏ|Ỵ|Ẏ|Ӳ|Ӯ|Ў|Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y', + '/ẙ|ʏ|ƴ|ɏ|ỵ|ẏ|ӳ|ӯ|ў|ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y', + '/В/' => 'V', + '/в/' => 'v', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Φ/' => 'F', + '/φ/' => 'f', + '/Χ/' => 'CH', + '/χ/' => 'ch', + '/Ź|Ż|Ž|Ζ|З/' => 'Z', + '/ź|ż|ž|ζ|з/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/' => 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/ƒ/' => 'f', + '/Ξ/' => 'KS', + '/ξ/' => 'ks', + '/Π/' => 'P', + '/π/' => 'p', + '/Β/' => 'V', + '/β/' => 'v', + '/Μ/' => 'M', + '/μ/' => 'm', + '/Ψ/' => 'PS', + '/ψ/' => 'ps', + '/Ё/' => 'Yo', + '/ё/' => 'yo', + '/Є/' => 'Ye', + '/є/' => 'ye', + '/Ї/' => 'Yi', + '/Ж/' => 'Zh', + '/ж/' => 'zh', + '/Х/' => 'Kh', + '/х/' => 'kh', + '/Ц/' => 'Ts', + '/ц/' => 'ts', + '/Ч/' => 'Ch', + '/ч/' => 'ch', + '/Ш/' => 'Sh', + '/ш/' => 'sh', + '/Щ/' => 'Shch', + '/щ/' => 'shch', + '/Ъ|ъ|Ь|ь/' => '', + '/Ю/' => 'Yu', + '/ю/' => 'yu', + '/Я/' => 'Ya', + '/я/' => 'ya' +); diff --git a/application/config/hooks.php b/application/config/hooks.php new file mode 100644 index 0000000..79c5c16 --- /dev/null +++ b/application/config/hooks.php @@ -0,0 +1,13 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/config/memcached.php b/application/config/memcached.php new file mode 100644 index 0000000..65a1496 --- /dev/null +++ b/application/config/memcached.php @@ -0,0 +1,19 @@ + array( + 'hostname' => '127.0.0.1', + 'port' => '11211', + 'weight' => '1', + ), +); diff --git a/application/config/migration.php b/application/config/migration.php new file mode 100644 index 0000000..4b585a6 --- /dev/null +++ b/application/config/migration.php @@ -0,0 +1,84 @@ +migration->current() this is the version that schema will +| be upgraded / downgraded to. +| +*/ +$config['migration_version'] = 0; + +/* +|-------------------------------------------------------------------------- +| Migrations Path +|-------------------------------------------------------------------------- +| +| Path to your migrations folder. +| Typically, it will be within your application path. +| Also, writing permission is required within the migrations path. +| +*/ +$config['migration_path'] = APPPATH.'migrations/'; diff --git a/application/config/mimes.php b/application/config/mimes.php new file mode 100644 index 0000000..b2e989f --- /dev/null +++ b/application/config/mimes.php @@ -0,0 +1,186 @@ + array('application/mac-binhex40', 'application/mac-binhex', 'application/x-binhex40', 'application/x-mac-binhex40'), + 'cpt' => 'application/mac-compactpro', + 'csv' => array('text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream', 'application/vnd.ms-excel', 'application/x-csv', 'text/x-csv', 'text/csv', 'application/csv', 'application/excel', 'application/vnd.msexcel', 'text/plain'), + 'bin' => array('application/macbinary', 'application/mac-binary', 'application/octet-stream', 'application/x-binary', 'application/x-macbinary'), + 'dms' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'exe' => array('application/octet-stream', 'application/x-msdownload'), + 'class' => 'application/octet-stream', + 'psd' => array('application/x-photoshop', 'image/vnd.adobe.photoshop'), + 'so' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => array('application/pdf', 'application/force-download', 'application/x-download', 'binary/octet-stream'), + 'ai' => array('application/pdf', 'application/postscript'), + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => array('application/vnd.ms-excel', 'application/msexcel', 'application/x-msexcel', 'application/x-ms-excel', 'application/x-excel', 'application/x-dos_ms_excel', 'application/xls', 'application/x-xls', 'application/excel', 'application/download', 'application/vnd.ms-office', 'application/msword'), + 'ppt' => array('application/powerpoint', 'application/vnd.ms-powerpoint', 'application/vnd.ms-office', 'application/msword'), + 'pptx' => array('application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/x-zip', 'application/zip'), + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'gzip' => 'application/x-gzip', + 'php' => array('application/x-httpd-php', 'application/php', 'application/x-php', 'text/php', 'text/x-php', 'application/x-httpd-php-source'), + 'php4' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'js' => array('application/x-javascript', 'text/plain'), + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => array('application/x-tar', 'application/x-gzip-compressed'), + 'z' => 'application/x-compress', + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'zip' => array('application/x-zip', 'application/zip', 'application/x-zip-compressed', 'application/s-compressed', 'multipart/x-zip'), + 'rar' => array('application/x-rar', 'application/rar', 'application/x-rar-compressed'), + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mpga' => 'audio/mpeg', + 'mp2' => 'audio/mpeg', + 'mp3' => array('audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'), + 'aif' => array('audio/x-aiff', 'audio/aiff'), + 'aiff' => array('audio/x-aiff', 'audio/aiff'), + 'aifc' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'rv' => 'video/vnd.rn-realvideo', + 'wav' => array('audio/x-wav', 'audio/wave', 'audio/wav'), + 'bmp' => array('image/bmp', 'image/x-bmp', 'image/x-bitmap', 'image/x-xbitmap', 'image/x-win-bitmap', 'image/x-windows-bmp', 'image/ms-bmp', 'image/x-ms-bmp', 'application/bmp', 'application/x-bmp', 'application/x-win-bitmap'), + 'gif' => 'image/gif', + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'jp2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'j2k' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpf' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpg2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpx' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'jpm' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'mj2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'mjp2' => array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'), + 'png' => array('image/png', 'image/x-png'), + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'heic' => 'image/heic', + 'heif' => 'image/heif', + 'css' => array('text/css', 'text/plain'), + 'html' => array('text/html', 'text/plain'), + 'htm' => array('text/html', 'text/plain'), + 'shtml' => array('text/html', 'text/plain'), + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'log' => array('text/plain', 'text/x-log'), + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'xml' => array('application/xml', 'text/xml', 'text/plain'), + 'xsl' => array('application/xml', 'text/xsl', 'text/xml'), + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'avi' => array('video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'), + 'movie' => 'video/x-sgi-movie', + 'doc' => array('application/msword', 'application/vnd.ms-office'), + 'docx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/msword', 'application/x-zip'), + 'dot' => array('application/msword', 'application/vnd.ms-office'), + 'dotx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/msword'), + 'xlsx' => array('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/vnd.ms-excel', 'application/msword', 'application/x-zip'), + 'word' => array('application/msword', 'application/octet-stream'), + 'xl' => 'application/excel', + 'eml' => 'message/rfc822', + 'json' => array('application/json', 'text/json'), + 'pem' => array('application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'), + 'p10' => array('application/x-pkcs10', 'application/pkcs10'), + 'p12' => 'application/x-pkcs12', + 'p7a' => 'application/x-pkcs7-signature', + 'p7c' => array('application/pkcs7-mime', 'application/x-pkcs7-mime'), + 'p7m' => array('application/pkcs7-mime', 'application/x-pkcs7-mime'), + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'crt' => array('application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'), + 'crl' => array('application/pkix-crl', 'application/pkcs-crl'), + 'der' => 'application/x-x509-ca-cert', + 'kdb' => 'application/octet-stream', + 'pgp' => 'application/pgp', + 'gpg' => 'application/gpg-keys', + 'sst' => 'application/octet-stream', + 'csr' => 'application/octet-stream', + 'rsa' => 'application/x-pkcs7', + 'cer' => array('application/pkix-cert', 'application/x-x509-ca-cert'), + '3g2' => 'video/3gpp2', + '3gp' => array('video/3gp', 'video/3gpp'), + 'mp4' => 'video/mp4', + 'm4a' => 'audio/x-m4a', + 'f4v' => array('video/mp4', 'video/x-f4v'), + 'flv' => 'video/x-flv', + 'webm' => 'video/webm', + 'aac' => array('audio/x-aac', 'audio/aac'), + 'm4u' => 'application/vnd.mpegurl', + 'm3u' => 'text/plain', + 'xspf' => 'application/xspf+xml', + 'vlc' => 'application/videolan', + 'wmv' => array('video/x-ms-wmv', 'video/x-ms-asf'), + 'au' => 'audio/x-au', + 'ac3' => 'audio/ac3', + 'flac' => 'audio/x-flac', + 'ogg' => array('audio/ogg', 'video/ogg', 'application/ogg'), + 'kmz' => array('application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'), + 'kml' => array('application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'), + 'ics' => 'text/calendar', + 'ical' => 'text/calendar', + 'zsh' => 'text/x-scriptzsh', + '7z' => array('application/x-7z-compressed', 'application/x-compressed', 'application/x-zip-compressed', 'application/zip', 'multipart/x-zip'), + '7zip' => array('application/x-7z-compressed', 'application/x-compressed', 'application/x-zip-compressed', 'application/zip', 'multipart/x-zip'), + 'cdr' => array('application/cdr', 'application/coreldraw', 'application/x-cdr', 'application/x-coreldraw', 'image/cdr', 'image/x-cdr', 'zz-application/zz-winassoc-cdr'), + 'wma' => array('audio/x-ms-wma', 'video/x-ms-asf'), + 'jar' => array('application/java-archive', 'application/x-java-application', 'application/x-jar', 'application/x-compressed'), + 'svg' => array('image/svg+xml', 'image/svg', 'application/xml', 'text/xml'), + 'vcf' => 'text/x-vcard', + 'srt' => array('text/srt', 'text/plain'), + 'vtt' => array('text/vtt', 'text/plain'), + 'ico' => array('image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'), + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'otf' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web' +); diff --git a/application/config/profiler.php b/application/config/profiler.php new file mode 100644 index 0000000..3436e93 --- /dev/null +++ b/application/config/profiler.php @@ -0,0 +1,14 @@ + my_controller/index +| my-controller/my-method -> my_controller/my_method +*/ +$route['default_controller'] = 'dashboard'; +$route['404_override'] = ''; +$route['translate_uri_dashes'] = FALSE; + +// untuk webhook absensi +$route['iclock/cdata'] = 'AttendanceWebhook/cdata'; + diff --git a/application/config/smileys.php b/application/config/smileys.php new file mode 100644 index 0000000..a9b9191 --- /dev/null +++ b/application/config/smileys.php @@ -0,0 +1,64 @@ + array('grin.gif', '19', '19', 'grin'), + ':lol:' => array('lol.gif', '19', '19', 'LOL'), + ':cheese:' => array('cheese.gif', '19', '19', 'cheese'), + ':)' => array('smile.gif', '19', '19', 'smile'), + ';-)' => array('wink.gif', '19', '19', 'wink'), + ';)' => array('wink.gif', '19', '19', 'wink'), + ':smirk:' => array('smirk.gif', '19', '19', 'smirk'), + ':roll:' => array('rolleyes.gif', '19', '19', 'rolleyes'), + ':-S' => array('confused.gif', '19', '19', 'confused'), + ':wow:' => array('surprise.gif', '19', '19', 'surprised'), + ':bug:' => array('bigsurprise.gif', '19', '19', 'big surprise'), + ':-P' => array('tongue_laugh.gif', '19', '19', 'tongue laugh'), + '%-P' => array('tongue_rolleye.gif', '19', '19', 'tongue rolleye'), + ';-P' => array('tongue_wink.gif', '19', '19', 'tongue wink'), + ':P' => array('raspberry.gif', '19', '19', 'raspberry'), + ':blank:' => array('blank.gif', '19', '19', 'blank stare'), + ':long:' => array('longface.gif', '19', '19', 'long face'), + ':ohh:' => array('ohh.gif', '19', '19', 'ohh'), + ':grrr:' => array('grrr.gif', '19', '19', 'grrr'), + ':gulp:' => array('gulp.gif', '19', '19', 'gulp'), + '8-/' => array('ohoh.gif', '19', '19', 'oh oh'), + ':down:' => array('downer.gif', '19', '19', 'downer'), + ':red:' => array('embarrassed.gif', '19', '19', 'red face'), + ':sick:' => array('sick.gif', '19', '19', 'sick'), + ':shut:' => array('shuteye.gif', '19', '19', 'shut eye'), + ':-/' => array('hmm.gif', '19', '19', 'hmmm'), + '>:(' => array('mad.gif', '19', '19', 'mad'), + ':mad:' => array('mad.gif', '19', '19', 'mad'), + '>:-(' => array('angry.gif', '19', '19', 'angry'), + ':angry:' => array('angry.gif', '19', '19', 'angry'), + ':zip:' => array('zip.gif', '19', '19', 'zipper'), + ':kiss:' => array('kiss.gif', '19', '19', 'kiss'), + ':ahhh:' => array('shock.gif', '19', '19', 'shock'), + ':coolsmile:' => array('shade_smile.gif', '19', '19', 'cool smile'), + ':coolsmirk:' => array('shade_smirk.gif', '19', '19', 'cool smirk'), + ':coolgrin:' => array('shade_grin.gif', '19', '19', 'cool grin'), + ':coolhmm:' => array('shade_hmm.gif', '19', '19', 'cool hmm'), + ':coolmad:' => array('shade_mad.gif', '19', '19', 'cool mad'), + ':coolcheese:' => array('shade_cheese.gif', '19', '19', 'cool cheese'), + ':vampire:' => array('vampire.gif', '19', '19', 'vampire'), + ':snake:' => array('snake.gif', '19', '19', 'snake'), + ':exclaim:' => array('exclaim.gif', '19', '19', 'exclaim'), + ':question:' => array('question.gif', '19', '19', 'question') + +); diff --git a/application/config/user_agents.php b/application/config/user_agents.php new file mode 100644 index 0000000..5e1f6af --- /dev/null +++ b/application/config/user_agents.php @@ -0,0 +1,222 @@ + 'Windows 10', + 'windows nt 6.3' => 'Windows 8.1', + 'windows nt 6.2' => 'Windows 8', + 'windows nt 6.1' => 'Windows 7', + 'windows nt 6.0' => 'Windows Vista', + 'windows nt 5.2' => 'Windows 2003', + 'windows nt 5.1' => 'Windows XP', + 'windows nt 5.0' => 'Windows 2000', + 'windows nt 4.0' => 'Windows NT 4.0', + 'winnt4.0' => 'Windows NT 4.0', + 'winnt 4.0' => 'Windows NT', + 'winnt' => 'Windows NT', + 'windows 98' => 'Windows 98', + 'win98' => 'Windows 98', + 'windows 95' => 'Windows 95', + 'win95' => 'Windows 95', + 'windows phone' => 'Windows Phone', + 'windows' => 'Unknown Windows OS', + 'android' => 'Android', + 'blackberry' => 'BlackBerry', + 'iphone' => 'iOS', + 'ipad' => 'iOS', + 'ipod' => 'iOS', + 'os x' => 'Mac OS X', + 'ppc mac' => 'Power PC Mac', + 'freebsd' => 'FreeBSD', + 'ppc' => 'Macintosh', + 'linux' => 'Linux', + 'debian' => 'Debian', + 'sunos' => 'Sun Solaris', + 'beos' => 'BeOS', + 'apachebench' => 'ApacheBench', + 'aix' => 'AIX', + 'irix' => 'Irix', + 'osf' => 'DEC OSF', + 'hp-ux' => 'HP-UX', + 'netbsd' => 'NetBSD', + 'bsdi' => 'BSDi', + 'openbsd' => 'OpenBSD', + 'gnu' => 'GNU/Linux', + 'unix' => 'Unknown Unix OS', + 'symbian' => 'Symbian OS' +); + + +// The order of this array should NOT be changed. Many browsers return +// multiple browser types so we want to identify the sub-type first. +$browsers = array( + 'OPR' => 'Opera', + 'Flock' => 'Flock', + 'Edge' => 'Edge', + 'Chrome' => 'Chrome', + // Opera 10+ always reports Opera/9.80 and appends Version/ to the user agent string + 'Opera.*?Version' => 'Opera', + 'Opera' => 'Opera', + 'MSIE' => 'Internet Explorer', + 'Internet Explorer' => 'Internet Explorer', + 'Trident.* rv' => 'Internet Explorer', + 'Shiira' => 'Shiira', + 'Firefox' => 'Firefox', + 'Chimera' => 'Chimera', + 'Phoenix' => 'Phoenix', + 'Firebird' => 'Firebird', + 'Camino' => 'Camino', + 'Netscape' => 'Netscape', + 'OmniWeb' => 'OmniWeb', + 'Safari' => 'Safari', + 'Mozilla' => 'Mozilla', + 'Konqueror' => 'Konqueror', + 'icab' => 'iCab', + 'Lynx' => 'Lynx', + 'Links' => 'Links', + 'hotjava' => 'HotJava', + 'amaya' => 'Amaya', + 'IBrowse' => 'IBrowse', + 'Maxthon' => 'Maxthon', + 'Ubuntu' => 'Ubuntu Web Browser' +); + +$mobiles = array( + // legacy array, old values commented out + 'mobileexplorer' => 'Mobile Explorer', +// 'openwave' => 'Open Wave', +// 'opera mini' => 'Opera Mini', +// 'operamini' => 'Opera Mini', +// 'elaine' => 'Palm', + 'palmsource' => 'Palm', +// 'digital paths' => 'Palm', +// 'avantgo' => 'Avantgo', +// 'xiino' => 'Xiino', + 'palmscape' => 'Palmscape', +// 'nokia' => 'Nokia', +// 'ericsson' => 'Ericsson', +// 'blackberry' => 'BlackBerry', +// 'motorola' => 'Motorola' + + // Phones and Manufacturers + 'motorola' => 'Motorola', + 'nokia' => 'Nokia', + 'nexus' => 'Nexus', + 'palm' => 'Palm', + 'iphone' => 'Apple iPhone', + 'ipad' => 'iPad', + 'ipod' => 'Apple iPod Touch', + 'sony' => 'Sony Ericsson', + 'ericsson' => 'Sony Ericsson', + 'blackberry' => 'BlackBerry', + 'cocoon' => 'O2 Cocoon', + 'blazer' => 'Treo', + 'lg' => 'LG', + 'amoi' => 'Amoi', + 'xda' => 'XDA', + 'mda' => 'MDA', + 'vario' => 'Vario', + 'htc' => 'HTC', + 'samsung' => 'Samsung', + 'sharp' => 'Sharp', + 'sie-' => 'Siemens', + 'alcatel' => 'Alcatel', + 'benq' => 'BenQ', + 'ipaq' => 'HP iPaq', + 'mot-' => 'Motorola', + 'playstation portable' => 'PlayStation Portable', + 'playstation 3' => 'PlayStation 3', + 'playstation vita' => 'PlayStation Vita', + 'hiptop' => 'Danger Hiptop', + 'nec-' => 'NEC', + 'panasonic' => 'Panasonic', + 'philips' => 'Philips', + 'sagem' => 'Sagem', + 'sanyo' => 'Sanyo', + 'spv' => 'SPV', + 'zte' => 'ZTE', + 'sendo' => 'Sendo', + 'nintendo dsi' => 'Nintendo DSi', + 'nintendo ds' => 'Nintendo DS', + 'nintendo 3ds' => 'Nintendo 3DS', + 'wii' => 'Nintendo Wii', + 'open web' => 'Open Web', + 'openweb' => 'OpenWeb', + 'meizu' => 'Meizu', + 'huawei' => 'Huawei', + 'xiaomi' => 'Xiaomi', + 'oppo' => 'Oppo', + 'vivo' => 'Vivo', + 'infinix' => 'Infinix', + + // Operating Systems + 'android' => 'Android', + 'symbian' => 'Symbian', + 'SymbianOS' => 'SymbianOS', + 'elaine' => 'Palm', + 'series60' => 'Symbian S60', + 'windows ce' => 'Windows CE', + + // Browsers + 'obigo' => 'Obigo', + 'netfront' => 'Netfront Browser', + 'openwave' => 'Openwave Browser', + 'mobilexplorer' => 'Mobile Explorer', + 'operamini' => 'Opera Mini', + 'opera mini' => 'Opera Mini', + 'opera mobi' => 'Opera Mobile', + 'fennec' => 'Firefox Mobile', + + // Other + 'digital paths' => 'Digital Paths', + 'avantgo' => 'AvantGo', + 'xiino' => 'Xiino', + 'novarra' => 'Novarra Transcoder', + 'vodafone' => 'Vodafone', + 'docomo' => 'NTT DoCoMo', + 'o2' => 'O2', + + // Fallback + 'mobile' => 'Generic Mobile', + 'wireless' => 'Generic Mobile', + 'j2me' => 'Generic Mobile', + 'midp' => 'Generic Mobile', + 'cldc' => 'Generic Mobile', + 'up.link' => 'Generic Mobile', + 'up.browser' => 'Generic Mobile', + 'smartphone' => 'Generic Mobile', + 'cellphone' => 'Generic Mobile' +); + +// There are hundreds of bots but these are the most common. +$robots = array( + 'googlebot' => 'Googlebot', + 'msnbot' => 'MSNBot', + 'baiduspider' => 'Baiduspider', + 'bingbot' => 'Bing', + 'slurp' => 'Inktomi Slurp', + 'yahoo' => 'Yahoo', + 'ask jeeves' => 'Ask Jeeves', + 'fastcrawler' => 'FastCrawler', + 'infoseek' => 'InfoSeek Robot 1.0', + 'lycos' => 'Lycos', + 'yandex' => 'YandexBot', + 'mediapartners-google' => 'MediaPartners Google', + 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler', + 'adsbot-google' => 'AdsBot Google', + 'feedfetcher-google' => 'Feedfetcher Google', + 'curious george' => 'Curious George', + 'ia_archiver' => 'Alexa Crawler', + 'MJ12bot' => 'Majestic-12', + 'Uptimebot' => 'Uptimebot', + 'UptimeRobot' => 'UptimeRobot' +); diff --git a/application/controllers/Accounts.php b/application/controllers/Accounts.php new file mode 100644 index 0000000..2479bf4 --- /dev/null +++ b/application/controllers/Accounts.php @@ -0,0 +1,168 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = [ + "active_menu" => "accounts", + "parent_accounts" => $this->db->get('accounts')->result() + ]; + + $this->load->view('partials/header', $data); + $this->load->view('accounts/layout', $data); + $this->load->view('partials/footer'); + } + + public function get_data() + { + $this->dm->setTable('accounts') + ->setColumn( + [null,'kode_akun','nama_akun','tipe','posisi','kategori',null], + ['kode_akun','nama_akun','tipe'], + ['kode_akun'=>'ASC'] + ); + + $list = $this->dm->get_datatables(); + + $data = []; + $no = $_POST['start']; + + foreach ($list as $row) { + $no++; + + $data[] = [ + $no, + $row->kode_akun, + $row->nama_akun, + ucfirst($row->tipe), + ucfirst($row->posisi), + ucfirst($row->kategori), + ' + + + ' + ]; + } + + echo json_encode([ + "draw" => $_POST['draw'], + "recordsTotal" => $this->dm->count_all(), + "recordsFiltered" => $this->dm->count_filtered(), + "data" => $data + ]); + } + + // ================= SAVE + public function save() + { + $tipe = $this->input->post('tipe'); + + $data = [ + 'kode_akun' => $this->input->post('kode_akun'), + 'nama_akun' => $this->input->post('nama_akun'), + 'tipe' => $tipe, + 'posisi' => in_array($tipe, ['asset','expense']) ? 'debit' : 'kredit', + 'kategori' => in_array($tipe, ['asset','liability','equity']) ? 'neraca' : 'laba_rugi', + 'parent_id' => $this->input->post('parent_id') ?: NULL + ]; + + $insert = $this->dm->setTable('accounts')->insert($data); + + if($insert){ + log_activity( + 'Accounts', + 'create', + 'Menambah akun: '.$data['kode_akun'].' - '.$data['nama_akun'] + ); + } + + echo json_encode([ + 'status' => $insert ? true : false, + 'message' => $insert ? 'Data berhasil disimpan' : 'Gagal simpan' + ]); + } + + // ================= UPDATE + public function update() + { + $id = $this->input->post('id'); + $tipe = $this->input->post('tipe'); + + $data = [ + 'kode_akun' => $this->input->post('kode_akun'), + 'nama_akun' => $this->input->post('nama_akun'), + 'tipe' => $tipe, + 'posisi' => in_array($tipe, ['asset','expense']) ? 'debit' : 'kredit', + 'kategori' => in_array($tipe, ['asset','liability','equity']) ? 'neraca' : 'laba_rugi', + 'parent_id' => $this->input->post('parent_id') ?: NULL + ]; + + $update = $this->dm->setTable('accounts')->update($id, $data); + + if($update){ + log_activity( + 'Accounts', + 'update', + 'Update akun ID '.$id.' menjadi: '.$data['kode_akun'].' - '.$data['nama_akun'] + ); + } + + echo json_encode([ + 'status' => $update ? true : false, + 'message' => $update ? 'Data berhasil diupdate' : 'Gagal update' + ]); + } + + public function detail($id) + { + $data = $this->dm->setTable('accounts')->find($id); + echo json_encode($data); + } + + public function delete($id) + { + // cek apakah jadi parent + $child = $this->db->where('parent_id',$id)->count_all_results('accounts'); + + if($child > 0){ + echo json_encode([ + 'status'=>false, + 'message'=>'Tidak bisa hapus, akun punya child' + ]); + return; + } + + $delete = $this->dm->setTable('accounts')->delete($id); + + if($delete){ + log_activity( + 'Accounts', + 'delete', + 'Menghapus akun ID '.$id, + 'success' + ); + } + + echo json_encode([ + 'status' => $delete ? true : false, + 'message' => $delete ? 'Data berhasil dihapus' : 'Gagal hapus' + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Asset.php b/application/controllers/Asset.php new file mode 100644 index 0000000..c01e41e --- /dev/null +++ b/application/controllers/Asset.php @@ -0,0 +1,343 @@ +load->database(); + } + + public function index(){ + $data = ["active_menu" => "asset"]; + $this->load->view('partials/header', $data); + $this->load->view('asset/layout'); + $this->load->view('partials/footer'); + } + + // ================= GET DATA ================= + public function get_data(){ + $data = $this->db + ->select('assets.*, lokasi_asset.nama as nama_lokasi') + ->from('assets') + ->join('lokasi_asset', 'lokasi_asset.id = assets.lokasi_asset_id', 'left') + ->order_by('assets.id','DESC') + ->get() + ->result(); + + $no = 1; + $result = []; + + foreach($data as $d){ + $result[] = [ + $no++, + $d->tanggal_perolehan, + $d->kode_asset ?? '-', + $d->nama_asset, + $d->nama_lokasi ?? '-', + number_format($d->nilai_perolehan), + $d->masa_manfaat . ' bln', + number_format($d->penyusutan_per_bulan), + $d->created_at, + ' + + + + ' + ]; + } + + echo json_encode(['data'=>$result]); + } + + // ================= DETAIL ASSET + HISTORY ================= + public function detail($id) + { + $data = $this->db + ->select('a.*, la.nama AS nama_lokasi') // ambil field tambahan dari lokasi_asset + ->from('assets a') + ->join('lokasi_asset la', 'a.lokasi_asset_id = la.id', 'left') + ->where('a.id', $id) + ->get() + ->row(); + + echo json_encode($data); + } + + public function get_history($asset_id){ + $data = $this->db + ->where('asset_id', $asset_id) + ->order_by('created_at', 'DESC') + ->get('asset_mutations') + ->result(); + + echo json_encode($data); + } + + // ================= SAVE MANUAL (1 UNIT) ================= + public function save(){ + $harga = preg_replace('/\D/','',$this->input->post('harga')); + $residu = preg_replace('/\D/','',$this->input->post('residu')); + $masa = (int)$this->input->post('masa'); + $account_debit_id = (int)$this->input->post('account_asset'); + $account_kredit_id = (int)$this->input->post('account_kas'); + $lokasi_asset_id = (int)$this->input->post('lokasi_asset'); + $tanggal = $this->input->post('tanggal'); + $nama_asset = $this->input->post('nama_asset'); + $keterangan = $this->input->post('keterangan'); + + $nilai_perolehan = $harga * 1; + $penyusutan = ($harga - $residu) / ($masa ?: 1); + + $this->db->trans_start(); + + $data = [ + 'kode_asset' => 'AST-'.time(), + 'nama_asset' => $nama_asset, + 'harga_per_unit' => $harga, + 'nilai_perolehan' => $nilai_perolehan, + 'tanggal_perolehan' => $tanggal, + 'masa_manfaat' => $masa, + 'nilai_residu' => $residu, + 'penyusutan_per_bulan' => $penyusutan, + 'nilai_buku' => $nilai_perolehan, + 'sumber' => 'manual', + 'account_debit_id' => $account_debit_id, + 'account_kredit_id' => $account_kredit_id, + 'lokasi_asset_id' => $lokasi_asset_id, + 'keterangan' => $keterangan + ]; + + $this->db->insert('assets',$data); + $asset_id = $this->db->insert_id(); + + // Journal + $this->db->insert('journals',[ + 'tanggal' => $tanggal, + 'no_ref' => 'AST-'.date('YmdHis').'-'.$asset_id, + 'keterangan' => 'Penambahan Aset '.$nama_asset, + 'ref_type' => 'assets', + 'ref_id' => $asset_id, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + $this->db->insert('journal_details',[ + 'journal_id' => $jid, + 'account_id' => $account_debit_id, + 'debit' => $nilai_perolehan, + 'kredit' => 0 + ]); + + $this->db->insert('journal_details',[ + 'journal_id' => $jid, + 'account_id' => $account_kredit_id, + 'debit' => 0, + 'kredit' => $nilai_perolehan + ]); + + // MUTATION PEROLEHAN + $this->db->insert('asset_mutations',[ + 'asset_id' => $asset_id, + 'tipe' => 'perolehan', + 'nilai' => $nilai_perolehan, + 'keterangan' => 'Perolehan manual: '.$keterangan + ]); + + $this->db->trans_complete(); + echo json_encode(['status'=>true]); + } + + // ================= SAVE FROM STOCK ================= + public function save_from_stock(){ + $item_id = $this->input->post('item_id'); + $account_debit_id = (int)$this->input->post('account_asset'); + $lokasi_asset_id = (int)$this->input->post('s_lokasi_asset'); + $masa = (int)$this->input->post('s_masa'); + $residu = preg_replace('/\D/','',$this->input->post('s_residu')); + $tanggal = $this->input->post('s_tanggal'); + + $item = $this->db->get_where('items',['id'=>$item_id])->row(); + + if(!$item){ + echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']); + return; + } + + if($item->stok < 1){ + echo json_encode(['status'=>false,'message'=>'Stok tidak cukup']); + return; + } + + $harga = $item->harga_beli; + $nama_asset = $item->nama_barang; + $nilai_perolehan = $harga; + $penyusutan = ($harga - $residu) / ($masa ?: 1); + + $this->db->trans_start(); + + // INSERT ASSET + $this->db->insert('assets',[ + 'item_id' => $item_id, + 'kode_asset' => 'AST-'.time(), + 'nama_asset' => $nama_asset, + 'harga_per_unit' => $harga, + 'nilai_perolehan' => $nilai_perolehan, + 'tanggal_perolehan' => $tanggal, + 'masa_manfaat' => $masa, + 'nilai_residu' => $residu, + 'penyusutan_per_bulan' => $penyusutan, + 'nilai_buku' => $nilai_perolehan, + 'sumber' => 'gudang', + 'account_debit_id' => $account_debit_id, + 'account_kredit_id' => 21, // Persediaan + 'lokasi_asset_id' => $lokasi_asset_id + ]); + + $asset_id = $this->db->insert_id(); + + // Journal + $this->db->insert('journals',[ + 'tanggal' => $tanggal, + 'no_ref' => 'AST-'.date('YmdHis').'-'.$asset_id, + 'keterangan' => 'Penambahan Aset dari Gudang '.$nama_asset, + 'ref_type' => 'assets', + 'ref_id' => $asset_id, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + // Debit Asset + $this->db->insert('journal_details',[ + 'journal_id' => $jid, + 'account_id' => $account_debit_id, + 'debit' => $nilai_perolehan, + 'kredit' => 0 + ]); + + // Kredit Persediaan + $this->db->insert('journal_details',[ + 'journal_id' => $jid, + 'account_id' => 21, + 'debit' => 0, + 'kredit' => $nilai_perolehan + ]); + + // UPDATE STOK ITEM (kurangi 1) + $this->db->where('id', $item_id) + ->set('stok', 'stok-1', FALSE) + ->update('items'); + + // MUTATION DARI GUDANG + $this->db->insert('asset_mutations',[ + 'asset_id' => $asset_id, + 'tipe' => 'perolehan', + 'nilai' => $nilai_perolehan, + 'keterangan' => 'Perolehan dari gudang: '.$nama_asset + ]); + + $this->db->trans_complete(); + + echo json_encode(['status'=>true]); + } + + // ================= UPDATE ================= + public function update(){ + $id = $this->input->post('id'); + $nama_asset = $this->input->post('nama_asset'); + $lokasi_asset_id = (int)$this->input->post('lokasi_asset'); + $keterangan = $this->input->post('keterangan'); + + $this->db->trans_start(); + + $data = [ + 'nama_asset' => $nama_asset, + 'lokasi_asset_id' => $lokasi_asset_id, + 'keterangan' => $keterangan + ]; + + $this->db->where('id',$id); + $this->db->update('assets',$data); + + // MUTATION EDIT + $this->db->insert('asset_mutations',[ + 'asset_id' => $id, + 'tipe' => 'penambahan', + 'nilai' => 0, + 'keterangan' => 'Edit asset: '.$nama_asset + ]); + + $this->db->trans_complete(); + echo json_encode(['status'=>true]); + } + + // ================= DELETE ================= + public function delete(){ + $id = $this->input->post('id'); + $alasan = $this->input->post('alasan'); + $keterangan = $this->input->post('keterangan'); + + $asset = $this->db->get_where('assets',['id'=>$id])->row(); + + $this->db->trans_start(); + + // Hapus journal + $journals = $this->db->get_where('journals', ['ref_type'=>'assets', 'ref_id'=>$id])->result(); + foreach($journals as $j){ + $this->db->delete('journal_details', ['journal_id'=>$j->id]); + $this->db->delete('journals', ['id'=>$j->id]); + } + + // Hapus asset + $this->db->delete('assets',['id'=>$id]); + + // MUTATION DELETE + $this->db->insert('asset_mutations',[ + 'asset_id' => $id, + 'tipe' => 'pengurangan', + 'nilai' => $asset->nilai_buku, + 'keterangan' => 'Penghapusan ('.$alasan.'): '.$keterangan + ]); + + $this->db->trans_complete(); + echo json_encode(['status'=>true]); + } + + // ================= GET DATA SELECT ================= + public function get_accounts_kas(){ + $data = $this->db + ->where_in('sub_tipe', ['kas', 'hutang']) + ->order_by('kode_akun', 'ASC') + ->get('accounts') + ->result(); + echo json_encode($data); + } + + public function get_accounts_asset(){ + $data = $this->db + ->group_start() + ->like('nama_akun', 'Aset', 'after') + ->group_end() + ->order_by('kode_akun', 'ASC') + ->get('accounts') + ->result(); + echo json_encode($data); + } + + public function get_items(){ + $data = $this->db + ->where('stok > 0') + ->get('items') + ->result(); + echo json_encode($data); + } + + public function get_lokasi_asset(){ + $data = $this->db + ->get('lokasi_asset') + ->result(); + echo json_encode($data); + } +} \ No newline at end of file diff --git a/application/controllers/AttendanceWebhook.php b/application/controllers/AttendanceWebhook.php new file mode 100644 index 0000000..f1a99c1 --- /dev/null +++ b/application/controllers/AttendanceWebhook.php @@ -0,0 +1,85 @@ +input->get('SN'); + + // ========================= + // RAW BODY DARI MESIN + // ========================= + $body = file_get_contents('php://input'); + + // ========================= + // LOG DIRECTORY + // ========================= + $logDir = APPPATH . 'logs/fingerprint/'; + + if (!is_dir($logDir)) { + mkdir($logDir, 0777, true); + } + + // ========================= + // FILE LOG + // ========================= + $logFile = $logDir . date('Y-m-d') . '.log'; + + // ========================= + // FORMAT LOG HEADER + // ========================= + $logText = "\n==================================================\n"; + $logText .= "TIME : " . date('Y-m-d H:i:s') . "\n"; + $logText .= "SN : " . $sn . "\n"; + $logText .= "IP : " . $this->input->ip_address() . "\n"; + $logText .= "DATA : \n"; + + // ========================= + // PARSE BARIS ABSENSI + // ========================= + $lines = explode("\n", trim($body)); + + foreach ($lines as $line) { + + $line = trim($line); + + if (empty($line)) { + continue; + } + + $logText .= $line . "\n"; + + // contoh parsing + $data = explode("\t", $line); + + /* + hasil biasanya: + [0] => PIN/User ID + [1] => Datetime + [2] => Status + */ + + } + + // ========================= + // SIMPAN LOG + // ========================= + file_put_contents($logFile, $logText, FILE_APPEND); + + // ========================= + // RESPONSE KE MESIN + // ========================= + echo "OK"; + } +} \ No newline at end of file diff --git a/application/controllers/Attendancemonitoring.php b/application/controllers/Attendancemonitoring.php new file mode 100644 index 0000000..8d3d5b2 --- /dev/null +++ b/application/controllers/Attendancemonitoring.php @@ -0,0 +1,65 @@ +load->database(); + } + + public function index() + { + $data = [ + "active_menu" => "attendance_monitoring" + ]; + + $this->load->view('partials/header', $data); + $this->load->view('employees/attendance_monitoring'); + $this->load->view('partials/footer'); + } + + public function get_data() + { + $date = $this->input->get('date'); + + if(!$date){ + $date = date('Y-m-d'); + } + + $data = $this->db->query(" + SELECT + a.*, + e.employee_code, + e.full_name, + s.shift_name + FROM k_attendances a + LEFT JOIN k_employees e ON e.id = a.employee_id + LEFT JOIN k_shifts s ON s.id = a.shift_id + WHERE a.attendance_date = '$date' + ORDER BY a.checkin_time DESC + ")->result(); + + $rows = []; + $no = 1; + + foreach($data as $d){ + + $rows[] = [ + $no++, + $d->employee_code, + $d->full_name, + $d->shift_name, + $d->checkin_time, + $d->checkout_time, + $d->attendance_status + ]; + } + + echo json_encode([ + 'data' => $rows + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Auth.php b/application/controllers/Auth.php new file mode 100644 index 0000000..068b2bf --- /dev/null +++ b/application/controllers/Auth.php @@ -0,0 +1,87 @@ +session->userdata('logged_in')) { + redirect('dashboard'); + } + + $this->load->view('auth/login'); + } + + public function process_login() + { + $username = $this->input->post('username'); + $password = $this->input->post('password'); + + $user = $this->db->get_where('v_users', ['username' => $username])->row(); + + if ($user && password_verify($password, $user->password)) { + + // Decode permissions + $permissions = json_decode($user->permissions, true); + if (!is_array($permissions)) { + $permissions = []; + } + + $session_data = [ + 'user_id' => $user->id, + 'nama' => $user->nama, + 'username' => $user->username, + 'role' => $user->nama_role, + 'permissions' => $permissions, + 'logged_in' => TRUE + ]; + + $this->session->set_userdata($session_data); + + // 🔥 LOG LOGIN BERHASIL + log_activity( + 'Auth', + 'login', + 'User '.$user->username.' berhasil login', + 'success' + ); + + redirect('dashboard'); + + } else { + + // 🔥 LOG LOGIN GAGAL + log_activity( + 'Auth', + 'login_failed', + 'Login gagal untuk username: '.$username, + 'failed' + ); + + $this->session->set_flashdata('error', 'Username atau Password salah!'); + redirect('auth'); + } + } + + public function logout() + { + $username = $this->session->userdata('username'); + + // 🔥 LOG LOGOUT (SEBELUM DESTROY SESSION) + log_activity( + 'Auth', + 'logout', + 'User '.$username.' logout', + 'success' + ); + + $this->session->sess_destroy(); + redirect('auth'); + } +} +?> \ No newline at end of file diff --git a/application/controllers/Basejurnal.php b/application/controllers/Basejurnal.php new file mode 100644 index 0000000..5bdb790 --- /dev/null +++ b/application/controllers/Basejurnal.php @@ -0,0 +1,290 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + // ================= INDEX + public function index() + { + + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = [ + "active_menu" => "basejurnal" + ]; + + $this->load->view('partials/header', $data); + $this->load->view('basejurnal/layout', $data); + $this->load->view('partials/footer'); + } + + // ================= DATATABLE + public function get_data() + { + $this->dm->setTable('base_journal') + ->setColumn( + [null,'kode','nama','deskripsi','is_active',null], + ['kode','nama'], + ['id'=>'DESC'] + ); + + $list = $this->dm->get_datatables(); + + $data = []; + $no = $_POST['start']; + + foreach ($list as $row) { + $no++; + + $status = $row->is_active ? 'Aktif' : 'Nonaktif'; + + $data[] = [ + $no, + $row->kode, + $row->nama, + $row->deskripsi, + $status, + ' + + + ' + ]; + } + + echo json_encode([ + "draw" => $_POST['draw'], + "recordsTotal" => $this->dm->count_all(), + "recordsFiltered" => $this->dm->count_filtered(), + "data" => $data + ]); + } + +public function save() +{ + $this->db->trans_start(); + + $kode = trim($this->input->post('kode')); + $nama = trim($this->input->post('nama')); + + if(empty($kode) || empty($nama)){ + echo json_encode([ + 'status' => false, + 'message' => 'Kode & Nama wajib diisi' + ]); + return; + } + + $header = [ + 'kode' => $kode, + 'nama' => $nama, + 'deskripsi' => $this->input->post('deskripsi'), + 'is_active' => 1 + ]; + + $this->dm->setTable('base_journal')->insert($header); + $base_id = $this->db->insert_id(); + + $accounts = $this->input->post('account_id'); + $posisi = $this->input->post('posisi'); + + if(!empty($accounts)){ + $detail = []; + + foreach ($accounts as $i => $acc) { + if(empty($acc)) continue; + + $detail[] = [ + 'base_journal_id' => $base_id, + 'account_id' => $acc, + 'posisi' => $posisi[$i] ?? 'debit', + 'urutan' => $i + 1 + ]; + } + + if(!empty($detail)){ + $this->db->insert_batch('base_journal_detail', $detail); + } + } + + $this->db->trans_complete(); + + if (!$this->db->trans_status()) { + echo json_encode([ + 'status' => false, + 'message' => 'Gagal simpan data' + ]); + return; + } + + log_activity( + 'base_journal', + 'create', + 'Membuat base journal kode: '.$kode, + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'Data berhasil disimpan' + ]); +} + +public function update() +{ + $id = $this->input->post('id'); + + if(empty($id)){ + echo json_encode([ + 'status' => false, + 'message' => 'ID tidak valid' + ]); + return; + } + + $this->db->trans_start(); + + $header = [ + 'kode' => trim($this->input->post('kode')), + 'nama' => trim($this->input->post('nama')), + 'deskripsi' => $this->input->post('deskripsi'), + ]; + + $this->dm->setTable('base_journal')->update($id, $header); + + // 🔥 DELETE OLD DETAIL + $this->db->where('base_journal_id', $id)->delete('base_journal_detail'); + + // 🔥 INSERT NEW DETAIL + $accounts = $this->input->post('account_id'); + $posisi = $this->input->post('posisi'); + + if(!empty($accounts)){ + $detail = []; + + foreach ($accounts as $i => $acc) { + if(empty($acc)) continue; + + $detail[] = [ + 'base_journal_id' => $id, + 'account_id' => $acc, + 'posisi' => $posisi[$i] ?? 'debit', + 'urutan' => $i + 1 + ]; + } + + if(!empty($detail)){ + $this->db->insert_batch('base_journal_detail', $detail); + } + } + + $this->db->trans_complete(); + + if (!$this->db->trans_status()) { + echo json_encode([ + 'status' => false, + 'message' => 'Gagal update data' + ]); + return; + } + + log_activity( + 'base_journal', + 'update', + 'Mengubah base journal ID: '.$id.' (kode: '.$header['kode'].')', + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'Data berhasil diupdate' + ]); +} + + // ================= DETAIL + public function detail($id) + { + $header = $this->dm->setTable('base_journal')->find($id); + + $detail = $this->db + ->select('d.*, a.nama_akun as account_name') + ->from('base_journal_detail d') + ->join('accounts a','a.id = d.account_id','left') // <-- GANTI DI SINI + ->where('d.base_journal_id',$id) + ->order_by('d.urutan','ASC') + ->get() + ->result(); + + echo json_encode([ + 'header' => $header, + 'detail' => $detail + ]); + } + + public function delete($id = null) + { + // set response JSON + $this->output->set_content_type('application/json'); + + if (empty($id)) { + echo json_encode([ + 'status' => false, + 'message' => 'ID tidak valid' + ]); + return; + } + + $this->db->trans_start(); + + // hapus detail dulu + $this->db->where('base_journal_id', $id)->delete('base_journal_detail'); + + // hapus header + $delete = $this->dm->setTable('base_journal')->delete($id); + + $this->db->trans_complete(); + + if (!$this->db->trans_status() || !$delete) { + echo json_encode([ + 'status' => false, + 'message' => 'Gagal hapus data' + ]); + return; + } + + // ================= LOG ACTIVITY ================= + log_activity( + 'base_journal', + 'delete', + 'Menghapus base journal ID: '.$id, + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'Data berhasil dihapus' + ]); + } + + public function get_list_account() + { + $data = $this->db->select('id, nama_akun, kode_akun') + ->from('accounts') // sesuaikan tabel kamu + ->order_by('kode_akun','ASC') + ->get() + ->result(); + + echo json_encode($data); + } +} \ No newline at end of file diff --git a/application/controllers/Bukubesar.php b/application/controllers/Bukubesar.php new file mode 100644 index 0000000..caa1817 --- /dev/null +++ b/application/controllers/Bukubesar.php @@ -0,0 +1,108 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = ["active_menu" => "buku_besar"]; + $this->load->view('partials/header', $data); + $this->load->view('buku_besar/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET ACCOUNTS (FILTER) + // ========================= + public function get_accounts() + { + $data = $this->dm->setTable('accounts') + ->order_by('kode_akun','ASC') + ->get(); + + echo json_encode($data); + } + + // ========================= + // GET DATA BUKU BESAR + // ========================= + public function get_data() + { + $account_id = $this->input->get('account_id'); + $start = $this->input->get('start_date'); + $end = $this->input->get('end_date'); + + $this->db->select(' + journals.tanggal, + journals.no_ref, + journals.keterangan, + journal_details.debit, + journal_details.kredit, + accounts.nama_akun, + accounts.tipe + '); + $this->db->from('journal_details'); + $this->db->join('journals','journals.id = journal_details.journal_id'); + $this->db->join('accounts','accounts.id = journal_details.account_id'); + + if ($account_id) { + $this->db->where('journal_details.account_id', $account_id); + } + + if ($start && $end) { + $this->db->where('journals.tanggal >=', $start); + $this->db->where('journals.tanggal <=', $end); + } + // 🔥 FILTER DEBIT/KREDIT TIDAK BOLEH 0 SEMUA + $this->db->group_start(); + $this->db->where('journal_details.debit >', 0); + $this->db->or_where('journal_details.kredit >', 0); + $this->db->group_end(); + + $this->db->order_by('journals.tanggal','ASC'); + + $rows = $this->db->get()->result(); + + // ========================= + // HITUNG SALDO BERJALAN + // ========================= + $saldo = 0; + $result = []; + + foreach ($rows as $r) { + + // LOGIKA SALDO BERDASARKAN TIPE AKUN + if (in_array($r->tipe, ['asset','expense'])) { + $saldo += ($r->debit - $r->kredit); + } else { + $saldo += ($r->kredit - $r->debit); + } + + $result[] = [ + 'tanggal' => $r->tanggal, + 'no_ref' => $r->no_ref, + 'keterangan' => $r->keterangan, + 'debit' => $r->debit, + 'kredit' => $r->kredit, + 'saldo' => $saldo + ]; + } + + echo json_encode($result); + } +} \ No newline at end of file diff --git a/application/controllers/Cameraqrscan.php b/application/controllers/Cameraqrscan.php new file mode 100644 index 0000000..d3ee79f --- /dev/null +++ b/application/controllers/Cameraqrscan.php @@ -0,0 +1,165 @@ +load->model('DynamicModel', 'dm'); + + // Cek apakah user sudah login + if (!$this->session->userdata('logged_in')) { + redirect('login'); + } + } + + public function index($type = 'masuk') + { + // pastikan hanya masuk/keluar + $type = strtolower($type); + if (!in_array($type, ['masuk', 'keluar'])) { + show_404(); + return; + } + + $data = [ + "active_menu" => "", + "scan_type" => $type // passing ke view + ]; + + $this->load->view('partials/header', $data); + $this->load->view('scan/qr_scan', $data); + $this->load->view('partials/footer'); + } + + public function get_data() + { + $qr_code = $this->input->post('qr_code'); + $scan_type = $this->input->post('scan_type'); // masuk / keluar + + if (!$qr_code) { + echo json_encode(['status' => false, 'message' => 'QR Code tidak ditemukan']); + return; + } + + // ============================================================ + // AMBIL DATA SISWA + // ============================================================ + $siswa = $this->db->get_where('v_siswa', ['qr_id' => $qr_code])->row_array(); + + if (!$siswa) { + echo json_encode([ + 'status' => false, + 'message' => 'Data siswa tidak ditemukan' + ]); + return; + } + + $id_siswa = $siswa['id']; + $id_kelas = $siswa['id_kelas']; + $id_kamar = $siswa['id_kamar']; + + // ============================================================ + // CEGAH DUPLIKAT 5 MENIT (PostgreSQL) + // ============================================================ + // Hitung waktu 5 menit yang lalu + $datetimeNow = date('Y-m-d H:i:s'); + $datetime5MenitLalu = date('Y-m-d H:i:s', strtotime('-5 minutes')); + + $cek = $this->db->query(" + SELECT id + FROM kunjungan + WHERE id_siswa = ? + AND created_at >= ? + ORDER BY created_at DESC + LIMIT 1 + ", [$id_siswa, $datetime5MenitLalu])->row_array(); + + if ($cek) { + echo json_encode([ + 'status' => true, + 'data' => $siswa, + 'scan_type' => $scan_type, + 'message' => "Sudah tercatat dalam 5 menit terakhir" + ]); + return; + } + + // ============================================================ + // AMBIL TEMPLATE PESAN + // ============================================================ + $template = $this->db->get_where('template_pesan', ['jenis' => $scan_type])->row_array(); + + if (!$template) { + echo json_encode([ + 'status' => false, + 'message' => 'Template pesan tidak ditemukan => ' . $scan_type + ]); + return; + } + + $pesan = $template['isi_template']; + $pesanType = $template['type']; + + + // ============================================================ + // REPLACE TEMPLATE {{column}} + // ============================================================ + foreach ($siswa as $key => $value) { + $pesan = str_replace('{{'.$key.'}}', $value, $pesan); + } + + // Tambahan otomatis + $pesan = str_replace('{{tanggal}}', date('Y-m-d'), $pesan); + $pesan = str_replace('{{jam}}', date('H:i:s'), $pesan); + + + // ============================================================ + // INSERT KUNJUNGAN + // ============================================================ + $this->db->insert('kunjungan', [ + 'id_siswa' => $id_siswa, + 'type' => $scan_type, + 'id_users' => $this->session->userdata('user_id'), + 'id_kelas' => $id_kelas, + 'id_kamar' => $id_kamar, + 'created_at' => date('Y-m-d H:i:s'), + 'media_type' => 'qr' + ]); + + $new_id = $this->db->insert_id(); + + + // ============================================================ + // INSERT NOTIFIKASI WA + // ============================================================ + $this->db->insert('notifikasi', [ + 'type' => $pesanType, + 'pesan' => $pesan, + 'status' => 'pending', + 'nomor_whatsapp' => $siswa['nomor_whatsapp'], + 'created_at' => date('Y-m-d H:i:s'), + ]); + + + // ============================================================ + // RESPONSE + // ============================================================ + $info = ($scan_type === 'masuk') + ? "Berhasil scan MASUK" + : "Berhasil scan KELUAR"; + + // tampilkan data lengkap dari v_kunjungan + $dataSiswa = $this->db->get_where('v_kunjungan_detail', ['id' => $new_id])->row(); + + echo json_encode([ + 'status' => true, + 'data' => $dataSiswa, + 'scan_type' => $scan_type, + 'message' => $info + ]); + } + + +} diff --git a/application/controllers/Cronjob.php b/application/controllers/Cronjob.php new file mode 100644 index 0000000..d162d68 --- /dev/null +++ b/application/controllers/Cronjob.php @@ -0,0 +1,140 @@ +load->database(); + + // ONLY CLI ACCESS + if (!$this->input->is_cli_request()) { + exit('No direct access allowed'); + } + } + + public function generate_insight() + { + $period = date('Y-m'); + + // ===================================================== + // 🔥 REPLACE MODE (hapus data lama dulu) + // ===================================================== + $this->db->where('period', $period); + $this->db->delete('ai_insights'); + + // ===================================================== + // 1. FINANCE - OVERDUE INVOICE + // ===================================================== + $overdue = $this->db->query(" + SELECT COUNT(*) as total + FROM invoices + WHERE status != 'paid' + AND jatuh_tempo < CURDATE() + AND deleted_at IS NULL + ")->row()->total; + + if ($overdue > 0) { + $this->db->insert('ai_insights', [ + 'type' => 'finance', + 'severity' => 'warning', + 'message' => "$overdue invoice sudah melewati jatuh tempo", + 'meta' => json_encode([ + 'overdue_count' => (int)$overdue + ]), + 'period' => $period + ]); + } + + // ===================================================== + // 2. FINANCE - CASHFLOW BULAN INI + // ===================================================== + $cash = $this->db->query(" + SELECT COALESCE(SUM(jd.debit),0) as cash_in + FROM journal_details jd + JOIN journals j ON j.id = jd.journal_id + WHERE MONTH(j.tanggal) = MONTH(CURDATE()) + AND YEAR(j.tanggal) = YEAR(CURDATE()) + ")->row()->cash_in; + + if ($cash > 0) { + $this->db->insert('ai_insights', [ + 'type' => 'finance', + 'severity' => ($cash > 10000000) ? 'info' : 'warning', + 'message' => "Cashflow bulan ini Rp " . number_format($cash), + 'meta' => json_encode([ + 'cash_in' => (float)$cash + ]), + 'period' => $period + ]); + } + + // ===================================================== + // 3. STOCK - KRITIS + // ===================================================== + $stocks = $this->db->query(" + SELECT + i.kode_id, + k.nama_barang, + SUM(i.stok) as total_stok, + k.limit_stock + FROM items i + JOIN kode_barang k ON k.id = i.kode_id + GROUP BY i.kode_id, k.nama_barang, k.limit_stock + HAVING total_stok <= k.limit_stock + ")->result(); + + foreach ($stocks as $s) { + $this->db->insert('ai_insights', [ + 'type' => 'stock', + 'severity' => 'critical', + 'message' => "Stok {$s->nama_barang} kritis ({$s->total_stok})", + 'meta' => json_encode([ + 'kode_id' => $s->kode_id, + 'item' => $s->nama_barang, + 'stok' => (int)$s->total_stok, + 'limit' => (int)$s->limit_stock + ]), + 'period' => $period + ]); + } + + // ===================================================== + // 4. CUSTOMER - PAYMENT RISK + // ===================================================== + $lateCustomers = $this->db->query(" + SELECT + c.nama, + COUNT(i.id) as total_invoice + FROM invoices i + JOIN customers c ON c.id = i.customer_id + WHERE i.status != 'paid' + AND i.jatuh_tempo < CURDATE() + GROUP BY i.customer_id + HAVING total_invoice > 2 + ")->result(); + + foreach ($lateCustomers as $c) { + $this->db->insert('ai_insights', [ + 'type' => 'customer', + 'severity' => 'warning', + 'message' => "Customer {$c->nama} memiliki {$c->total_invoice} invoice overdue", + 'meta' => json_encode([ + 'customer' => $c->nama, + 'overdue_invoice' => (int)$c->total_invoice + ]), + 'period' => $period + ]); + } + + // ===================================================== + // RESPONSE + // ===================================================== + echo json_encode([ + 'status' => true, + 'message' => 'AI Insight generated (REPLACE MODE)', + 'period' => $period + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Customers.php b/application/controllers/Customers.php new file mode 100644 index 0000000..af6db61 --- /dev/null +++ b/application/controllers/Customers.php @@ -0,0 +1,150 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + $data = ["active_menu" => "customers"]; + $this->load->view('partials/header', $data); + $this->load->view('customers/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // DATATABLE + // ========================= + public function get_data() + { + $this->dm->setTable('customers') + ->setColumn( + [null,'nama','alamat','telp','email',null], + ['nama','alamat','telp','email'], + ['id'=>'DESC'] + ); + + $list = $this->dm->get_datatables(); + + $data = []; + $no = $_POST['start']; + + foreach ($list as $row) { + $no++; + + $aksi = ' + + + '; + + $data[] = [ + $no, + $row->nama, + $row->alamat, + $row->telp, + $row->email, + $aksi + ]; + } + + echo json_encode([ + "draw" => $_POST['draw'], + "recordsTotal" => $this->dm->count_all(), + "recordsFiltered" => $this->dm->count_filtered(), + "data" => $data + ]); + } + + // ========================= + // SAVE + // ========================= + public function save() + { + $data = [ + 'nama' => $this->input->post('nama'), + 'alamat' => $this->input->post('alamat'), + 'telp' => $this->input->post('telp'), + 'email' => $this->input->post('email') + ]; + + if (!$data['nama']) { + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->dm->setTable('customers')->insert($data); + + // ================= LOG ACTIVITY ================= + log_activity( + 'customers', + 'create', + 'Menambahkan customer: '.$data['nama'], + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Data berhasil disimpan']); + } + + // ========================= + // DETAIL + // ========================= + public function detail($id) + { + $data = $this->dm->setTable('customers')->get_by_id($id); + echo json_encode($data); + } + + // ========================= + // UPDATE + // ========================= + public function update() + { + $id = $this->input->post('id'); + + $data = [ + 'nama' => $this->input->post('nama'), + 'alamat' => $this->input->post('alamat'), + 'telp' => $this->input->post('telp'), + 'email' => $this->input->post('email') + ]; + + $this->dm->setTable('customers')->update($id, $data); + + // ================= LOG ACTIVITY ================= + log_activity( + 'customers', + 'update', + 'Mengubah customer ID: '.$id.' ('.$data['nama'].')', + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Data berhasil diupdate']); + } + + // ========================= + // DELETE + // ========================= + public function delete($id) + { + $this->dm->setTable('customers')->delete($id); + + // ================= LOG ACTIVITY ================= + log_activity( + 'customers', + 'delete', + 'Menghapus customer: '.($customer->nama ?? 'ID '.$id), + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Data berhasil dihapus']); + } +} \ No newline at end of file diff --git a/application/controllers/Dashboard.php b/application/controllers/Dashboard.php new file mode 100644 index 0000000..7e96c2f --- /dev/null +++ b/application/controllers/Dashboard.php @@ -0,0 +1,279 @@ +load->library('session'); + + // Cek apakah user sudah login + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + $this->db->select(' + accounts.id, + accounts.kode_akun, + accounts.nama_akun, + accounts.tipe, + COALESCE(SUM(journal_details.debit),0) as debit, + COALESCE(SUM(journal_details.kredit),0) as kredit + '); + $this->db->from('accounts'); + $this->db->join('journal_details','accounts.id = journal_details.account_id','left'); + $this->db->where('accounts.kategori','laba_rugi'); + $this->db->where('accounts.is_active',1); + $this->db->group_by('accounts.id'); + $this->db->order_by('accounts.kode_akun','ASC'); + + $rows = $this->db->get()->result(); + + $revenue = 0; + $expense = 0; + + foreach ($rows as $r) { + + // 🔥 NORMALISASI SALDO BERDASARKAN POSISI + if ($r->tipe == 'revenue') { + $saldo = $r->kredit - $r->debit; + $revenue += $saldo; + } + + if ($r->tipe == 'expense') { + $saldo = $r->debit - $r->kredit; + $expense += $saldo; + } + } + + $summary = [ + 'total_revenue' => $revenue, + 'total_expense' => $expense, + 'net_profit' => $revenue - $expense + ]; + + $today = date('Y-m'); + $ai_insight = $this->db + ->select('type, severity, message, meta, created_at') + ->where('period', $today) + ->order_by(" + CASE severity + WHEN 'critical' THEN 1 + WHEN 'warning' THEN 2 + WHEN 'info' THEN 3 + END + ", '', false) + ->order_by('created_at', 'DESC') + ->limit(20) + ->get('ai_insights') + ->result(); + + $data = [ + "active_menu" => "dashboard", + "summary" => $summary, + "ai_insight" => $ai_insight + ]; + + $this->load->view('partials/header', $data); + $this->load->view('dashboard/layout', $data); + $this->load->view('partials/footer'); + } + + public function get_activity() + { + $this->db->select('activity_logs.*, users.nama'); + $this->db->from('activity_logs'); + $this->db->join('users','users.id = activity_logs.user_id','left'); + + if ($this->session->userdata('role') != 'Admin') { + $this->db->where('user_id',$this->session->userdata('user_id')); + } + + $this->db->order_by('created_at','DESC'); + $this->db->limit(10); + + $data = $this->db->get()->result(); + + echo json_encode($data); + } + + public function log_activity() + { + $data = [ + "active_menu" => "dashboard" + ]; + + $this->load->view('partials/header', $data); + $this->load->view('dashboard/activity_logs', $data); + $this->load->view('partials/footer'); + + } + + public function get_activity_datatable() + { + $this->load->database(); + + $draw = $this->input->post('draw'); + $start = $this->input->post('start'); + $length = $this->input->post('length'); + $search = $this->input->post('search')['value'] ?? ''; + + // ========================= + // BASE QUERY + // ========================= + $this->db->select(' + activity_logs.*, + users.nama + '); + $this->db->from('activity_logs'); + $this->db->join('users', 'users.id = activity_logs.user_id', 'left'); + + // ========================= + // SEARCH + // ========================= + if (!empty($search)) { + $this->db->group_start(); + $this->db->like('activity_logs.module', $search); + $this->db->or_like('activity_logs.action', $search); + $this->db->or_like('activity_logs.description', $search); + $this->db->or_like('activity_logs.status', $search); + $this->db->or_like('users.nama', $search); + $this->db->group_end(); + } + + // ========================= + // COUNT FILTERED + // ========================= + $filtered_query = clone $this->db; + $recordsFiltered = $filtered_query->count_all_results('', false); + + // ========================= + // ORDER + // ========================= + $this->db->order_by('activity_logs.id', 'DESC'); + + // ========================= + // LIMIT + // ========================= + if ($length != -1) { + $this->db->limit($length, $start); + } + + $query = $this->db->get(); + $data = $query->result(); + + // ========================= + // TOTAL RECORD + // ========================= + $recordsTotal = $this->db->count_all('activity_logs'); + + // ========================= + // FORMAT OUTPUT + // ========================= + $result = []; + + foreach ($data as $row) { + $result[] = [ + 'created_at' => $row->created_at, + 'nama' => $row->nama ?? 'System', + 'module' => $row->module, + 'action' => $row->action, + 'description' => $row->description, + 'status' => $row->status, + 'ip_address' => $row->ip_address + ]; + } + + echo json_encode([ + "draw" => intval($draw), + "recordsTotal" => $recordsTotal, + "recordsFiltered" => $recordsFiltered, + "data" => $result + ]); + } + + public function get_chart() + { + $year = $this->input->get('year'); + + // 🔥 DEFAULT: tahun berjalan + if (empty($year)) { + $year = date('Y'); + } + + $this->db->select(' + journals.tanggal, + accounts.tipe, + SUM(journal_details.debit) as total_debit, + SUM(journal_details.kredit) as total_kredit + '); + $this->db->from('journal_details'); + $this->db->join('journals', 'journals.id = journal_details.journal_id', 'left'); + $this->db->join('accounts', 'accounts.id = journal_details.account_id', 'left'); + $this->db->where('accounts.kategori', 'laba_rugi'); + $this->db->where('accounts.is_active', 1); + + // 🔥 pakai tahun default / filter + $this->db->where('YEAR(journals.tanggal)', $year); + + $this->db->group_by(['journals.tanggal', 'accounts.tipe']); + $this->db->order_by('journals.tanggal', 'ASC'); + + $rows = $this->db->get()->result(); + + $map = []; + + foreach ($rows as $r) { + + $date = $r->tanggal; + + if (!isset($map[$date])) { + $map[$date] = [ + 'revenue' => 0, + 'expense' => 0 + ]; + } + + if ($r->tipe == 'revenue') { + $map[$date]['revenue'] += ($r->total_kredit - $r->total_debit); + } + + if ($r->tipe == 'expense') { + $map[$date]['expense'] += ($r->total_debit - $r->total_kredit); + } + } + + $labels = []; + $revenue = []; + $expense = []; + + foreach ($map as $date => $val) { + $labels[] = $date; + $revenue[] = (float)$val['revenue']; + $expense[] = (float)$val['expense']; + } + + echo json_encode([ + 'year' => $year, // optional biar debugging enak + 'labels' => $labels, + 'revenue' => $revenue, + 'expense' => $expense + ]); + } + + public function get_years() + { + $this->db->select('DISTINCT YEAR(tanggal) as year'); + $this->db->from('journals'); + $this->db->order_by('year', 'DESC'); + + $data = $this->db->get()->result(); + + echo json_encode($data); + } + +} diff --git a/application/controllers/Employees.php b/application/controllers/Employees.php new file mode 100644 index 0000000..6229d12 --- /dev/null +++ b/application/controllers/Employees.php @@ -0,0 +1,450 @@ +load->database(); + $this->load->helper(['url', 'file']); + + date_default_timezone_set('Asia/Jakarta'); + } + + // ========================================================= + // INDEX + // ========================================================= + public function index() + { + $data = [ + "active_menu" => "employees" + ]; + + $data['departments'] = $this->db + ->where('is_active',1) + ->order_by('department_name','ASC') + ->get('k_departments') + ->result(); + + $data['positions'] = $this->db + ->where('is_active',1) + ->order_by('position_name','ASC') + ->get('k_positions') + ->result(); + + $data['branches'] = $this->db + ->order_by('branch_name','ASC') + ->get('k_branches') + ->result(); + + $this->load->view('partials/header', $data); + $this->load->view('employees/employees', $data); + $this->load->view('partials/footer'); + } + + // ========================================================= + // GET ALL + // ========================================================= + public function getAll() + { + try { + + $data = $this->db + ->select(" + e.*, + + d.department_name, + p.position_name, + b.branch_name + ") + ->from('k_employees e') + + ->join( + 'k_departments d', + 'd.id = e.department_id', + 'left' + ) + + ->join( + 'k_positions p', + 'p.id = e.position_id', + 'left' + ) + + ->join( + 'k_branches b', + 'b.id = e.branch_id', + 'left' + ) + + ->order_by('e.department_id','ASC') + ->get() + ->result(); + + return $this->json([ + 'status' => true, + 'data' => $data + ]); + + } catch (Exception $e) { + + return $this->json([ + 'status' => false, + 'message' => $e->getMessage(), + 'data' => [] + ]); + } + } + + // ========================================================= + // DETAIL + // ========================================================= + public function detail($id = null) + { + try { + + $data = $this->db + ->select(" + e.*, + + d.department_name, + p.position_name, + b.branch_name + ") + ->from('k_employees e') + + ->join( + 'k_departments d', + 'd.id = e.department_id', + 'left' + ) + + ->join( + 'k_positions p', + 'p.id = e.position_id', + 'left' + ) + + ->join( + 'k_branches b', + 'b.id = e.branch_id', + 'left' + ) + + ->where('e.id',$id) + ->get() + ->row(); + + if (!$data) { + + return $this->json([ + 'status' => false, + 'message' => 'Data tidak ditemukan' + ]); + } + + return $this->json([ + 'status' => true, + 'data' => $data + ]); + + } catch (Exception $e) { + + return $this->json([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } + } + + // ========================================================= + // STORE + // ========================================================= + public function store() + { + try { + + if (empty(trim($this->input->post('full_name')))) { + + return $this->json([ + 'status' => false, + 'message'=> 'Nama wajib diisi' + ]); + } + + if (empty($this->input->post('join_date'))) { + + return $this->json([ + 'status' => false, + 'message'=> 'Tanggal masuk wajib diisi' + ]); + } + + $employee_code = $this->generateEmployeeCode(); + + $photo = $this->uploadPhoto(); + + $data = $this->prepareData(); + + $data['employee_code'] = $employee_code; + $data['photo'] = $photo; + + $this->db->insert('k_employees',$data); + + return $this->json([ + 'status' => true, + 'message' => 'Berhasil simpan data' + ]); + + } catch (Exception $e) { + + return $this->json([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } + } + + // ========================================================= + // UPDATE + // ========================================================= + public function update($id = null) + { + try { + + $employee = $this->db + ->get_where('k_employees',['id'=>$id]) + ->row(); + + if (!$employee) { + + return $this->json([ + 'status' => false, + 'message' => 'Data tidak ditemukan' + ]); + } + + $data = $this->prepareData(); + + if (!empty($_FILES['photo_file']['name'])) { + + $photo = $this->uploadPhoto(); + + if ( + !empty($employee->photo) + && + file_exists(FCPATH . $employee->photo) + ) { + unlink(FCPATH . $employee->photo); + } + + $data['photo'] = $photo; + } + + $this->db + ->where('id',$id) + ->update('k_employees',$data); + + return $this->json([ + 'status' => true, + 'message' => 'Berhasil update data' + ]); + + } catch (Exception $e) { + + return $this->json([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } + } + + // ========================================================= + // DELETE + // ========================================================= + public function delete($id = null) + { + try { + + $employee = $this->db + ->get_where('k_employees',['id'=>$id]) + ->row(); + + if (!$employee) { + + return $this->json([ + 'status' => false, + 'message' => 'Data tidak ditemukan' + ]); + } + + if ( + !empty($employee->photo) + && + file_exists(FCPATH . $employee->photo) + ) { + unlink(FCPATH . $employee->photo); + } + + $this->db->delete('k_employees',[ + 'id'=>$id + ]); + + return $this->json([ + 'status' => true, + 'message' => 'Berhasil hapus data' + ]); + + } catch (Exception $e) { + + return $this->json([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } + } + + // ========================================================= + // GENERATE CODE + // ========================================================= + private function generateEmployeeCode() + { + $last = $this->db + ->order_by('id','DESC') + ->get('k_employees') + ->row(); + + $number = $last ? $last->id + 1 : 1; + + return 'RJN-' . str_pad($number,5,'0',STR_PAD_LEFT); + } + + // ========================================================= + // UPLOAD PHOTO + // ========================================================= + private function uploadPhoto() + { + if (empty($_FILES['photo_file']['name'])) { + return null; + } + + $path = FCPATH . 'uploads/karyawan/'; + + if (!is_dir($path)) { + mkdir($path,0777,true); + } + + $config['upload_path'] = './uploads/karyawan/'; + $config['allowed_types'] = 'jpg|jpeg|png|webp'; + $config['max_size'] = 2048; + $config['encrypt_name'] = true; + + $this->load->library('upload',$config); + + if (!$this->upload->do_upload('photo_file')) { + + throw new Exception( + strip_tags( + $this->upload->display_errors() + ) + ); + } + + $upload = $this->upload->data(); + + return 'uploads/karyawan/' . $upload['file_name']; + } + + // ========================================================= + // PREPARE DATA + // ========================================================= + private function prepareData() + { + return [ + + 'nik' => trim($this->input->post('nik')), + + 'full_name' => trim( + $this->input->post('full_name') + ), + + 'gender' => $this->input->post('gender'), + + 'birth_place' => trim( + $this->input->post('birth_place') + ), + + 'birth_date' => $this->input->post('birth_date'), + + 'phone' => trim( + $this->input->post('phone') + ), + + 'email' => trim( + $this->input->post('email') + ), + + 'address' => trim( + $this->input->post('address') + ), + + 'join_date' => $this->input->post('join_date'), + + 'resign_date' => $this->input->post('resign_date'), + + 'employment_status' => $this->input->post('employment_status'), + + 'marital_status' => $this->input->post('marital_status'), + + 'religion' => trim( + $this->input->post('religion') + ), + + 'department_id' => $this->input->post('department_id'), + + 'position_id' => $this->input->post('position_id'), + + 'branch_id' => $this->input->post('branch_id'), + + 'basic_salary' => $this->input->post('basic_salary') ?: 0, + + 'bank_name' => trim( + $this->input->post('bank_name') + ), + + 'bank_account_number' => trim( + $this->input->post('bank_account_number') + ), + + 'bank_account_name' => trim( + $this->input->post('bank_account_name') + ), + + 'bpjs_kesehatan_number' => trim( + $this->input->post('bpjs_kesehatan_number') + ), + + 'bpjs_ketenagakerjaan_number' => trim( + $this->input->post('bpjs_ketenagakerjaan_number') + ), + + 'fingerprint_user_id' => trim( + $this->input->post('fingerprint_user_id') + ), + + 'is_active' => $this->input->post('is_active') ?: 1 + ]; + } + + // ========================================================= + // JSON + // ========================================================= + private function json($data) + { + return $this->output + ->set_content_type('application/json') + ->set_output(json_encode($data)); + } +} \ No newline at end of file diff --git a/application/controllers/Generateinvoice.php b/application/controllers/Generateinvoice.php new file mode 100644 index 0000000..14e9ed5 --- /dev/null +++ b/application/controllers/Generateinvoice.php @@ -0,0 +1,188 @@ +db->get('pengaturan')->row_array(); // 🔥 FIX: tidak pakai GET server_id + + if(!$invp){ + show_error('Data perusahaan tidak ditemukan'); + } + + $nama = $invp['nama_perusahaan']; + $alamat = $invp['alamat_perusahaan']; + $telepon = $invp['telepon_perusahaan']; + $Keterangan = $invp['catatan_perusahaan']; + $Pembuat = $invp['nama_bendahara']; + $logo = base_url('uploads/img/logo-ljn.png'); + + // ========================= + // 🔥 GET INVOICE + CUSTOMER + // ========================= + $invi = $this->db->select('invoices.*, customers.nama, customers.alamat, customers.telp') + ->from('invoices') + ->join('customers', 'customers.id = invoices.customer_id', 'left') + ->where('invoices.id', $invoice_id) + ->get() + ->row_array(); + + // ========================= + // VALIDASI + // ========================= + if(!$invi){ + show_error('Invoice tidak ditemukan'); + } + + // ========================= + // MAPPING DATA + // ========================= + $customerAddress = $invi['alamat'] ?? '-'; + $customerName = $invi['nama'] ?? '-'; + $customerNumber = $invi['telp'] ?? '-'; + + $invoiceNumber = $invi['no_invoice'] ?? '-'; + $invoiceDate = $invi['tanggal'] ?? '-'; // 🔥 sesuaikan field kamu + $invoiceLimit = $invi['jatuh_tempo'] ?? '-'; // 🔥 sesuaikan juga + + // ========================= + // 🔥 DETAIL ITEM + // ========================= + $header = ['No','Tanggal','Item','Deskripsi','Qty','Harga', 'Subtotal']; + $data = []; + + // 🔥 NOTE: sesuaikan tabel kamu (invoice_details / invoice_list) + // $detail = $this->db->get_where('invoice_details', [ + // 'invoice_id' => $invoice_id + // ])->result_array(); + + $detail = $this->db + ->where('invoice_id', $invoice_id) + ->order_by('tanggal', 'ASC') + ->get('invoice_details') + ->result_array(); + + $no = 1; + foreach($detail as $d){ + $data[] = [ + $no++, + $d['tanggal'], + $d['nama_item'], + $d['keterangan'], + $d['qty'], + number_format($d['harga']), + number_format($d['subtotal']) + ]; + } + + // ========================= + // 🔥 TOTAL + // ========================= + $total = (float)$invi['total']; + + $this->db->select_sum('sisa_piutang'); + $this->db->where('customer_id', $invi['customer_id']); + $this->db->where('status !=', 'paid'); + $this->db->where('id !=', $invoice_id); // exclude invoice sekarang + + $tunggakan = $this->db->get('invoices')->row()->sisa_piutang ?? 0; + + // Ngambil Data Tunggakan bulan lalu jika ada. + $keteranganlain = 'Akumulasi Tunggakan Invoice Bulan Lalu'; + $nom_lainnya = $tunggakan; + + $subtotal = number_format($total); + $lainnya = number_format($nom_lainnya); + $totalAmount = number_format($total + $nom_lainnya); + $terbilang = trim($this->terbilang($total + $nom_lainnya)).' rupiah'; + + $filename = 'INV-'.$customerName.'-'.$invoiceNumber.'.pdf'; + + // ========================= + // 🔥 LOAD FPDF + // ========================= + require_once(APPPATH.'third_party/fpdf/fpdf.php'); + require_once(APPPATH.'libraries/PDF_Invoice.php'); + + $pdf = new PDF(); + + // 🔥 FIX PATH LOGO (WAJIB) + $logo_path = FCPATH.'img/user/'.$logo; + if(!file_exists($logo_path)){ + $logo_path = ''; // biar gak error kalau logo tidak ada + } + + $pdf->setHeaderData($nama, $alamat, $telepon, $logo); + $pdf->AddPage(); + $pdf->SetFont('Arial','',12); + + $pdf->CustomerDetails( + $customerName, + $customerAddress, + $invoiceNumber, + $invoiceDate, + $customerNumber, + $invoiceLimit + ); + + $pdf->InvoiceTable($header, $data); + $pdf->Total($subtotal,$lainnya,$totalAmount,$terbilang,$keteranganlain); + $pdf->Keterangan($Keterangan); + $pdf->Pembuat($Pembuat); + + // ========================= + // 🔥 OUTPUT LANGSUNG (TANPA SIMPAN) + // ========================= + $pdf->Output('I', $filename); + + + // ========================= + // LOG ACTIVITY + // ========================= + log_activity( + 'invoice', + 'generate', + 'Generate invoice #' . $invoiceNumber . ' untuk ' . $customerName, + 'success' + ); + + exit; + } + + // ========================= + // 🔥 TERBILANG + // ========================= + private function terbilang($angka) + { + $angka = abs($angka); + $baca = ["", "satu", "dua", "tiga", "empat", "lima", "enam", "tujuh", "delapan", "sembilan", "sepuluh", "sebelas"]; + + if ($angka < 12) { + return " " . $baca[$angka]; + } elseif ($angka < 20) { + return $this->terbilang($angka - 10) . " belas"; + } elseif ($angka < 100) { + return $this->terbilang(intval($angka / 10)) . " puluh" . $this->terbilang($angka % 10); + } elseif ($angka < 200) { + return " seratus" . $this->terbilang($angka - 100); + } elseif ($angka < 1000) { + return $this->terbilang(intval($angka / 100)) . " ratus" . $this->terbilang($angka % 100); + } elseif ($angka < 2000) { + return " seribu" . $this->terbilang($angka - 1000); + } elseif ($angka < 1000000) { + return $this->terbilang(intval($angka / 1000)) . " ribu" . $this->terbilang($angka % 1000); + } elseif ($angka < 1000000000) { + return $this->terbilang(intval($angka / 1000000)) . " juta" . $this->terbilang($angka % 1000000); + } + + return ""; + } + +} \ No newline at end of file diff --git a/application/controllers/Generateschedule.php b/application/controllers/Generateschedule.php new file mode 100644 index 0000000..d1de16b --- /dev/null +++ b/application/controllers/Generateschedule.php @@ -0,0 +1,768 @@ +load->database(); + + date_default_timezone_set('Asia/Jakarta'); + } + + // ===================================================== + // INDEX + // ===================================================== + public function index() + { + $data = [ + "active_menu" => "generate_schedule" + ]; + + // ============================================ + // POSITIONS + // ============================================ + $data['positions'] = $this->db + ->where('is_active',1) + ->order_by('position_name','ASC') + ->get('k_positions') + ->result(); + + // ============================================ + // SHIFTS + // ============================================ + $data['shifts'] = $this->db + ->order_by('shift_code','ASC') + ->get('k_shifts') + ->result(); + + $this->load->view('partials/header', $data); + $this->load->view('employees/generate_schedule', $data); + $this->load->view('partials/footer'); + } + +// ===================================================== +// CALENDAR DATA +// ===================================================== +public function calendar_data() +{ + $month = (int)$this->input->get('month'); + $year = (int)$this->input->get('year'); + + if(empty($month)){ + $month = date('n'); + } + + if(empty($year)){ + $year = date('Y'); + } + + $firstDate = $year . '-' + . str_pad($month,2,'0',STR_PAD_LEFT) + . '-01'; + + // ============================================ + // START MONDAY + // ============================================ + $startDate = date( + 'Y-m-d', + strtotime('monday this week', strtotime($firstDate)) + ); + + if(date('N', strtotime($firstDate)) == 1){ + $startDate = $firstDate; + } + + // ============================================ + // END SUNDAY + // ============================================ + $lastDate = date( + 'Y-m-t', + strtotime($firstDate) + ); + + $endDate = date( + 'Y-m-d', + strtotime('sunday this week', strtotime($lastDate)) + ); + + $days = []; + + $current = strtotime($startDate); + $end = strtotime($endDate); + + while($current <= $end){ + + $date = date('Y-m-d', $current); + + // ======================================== + // GET SCHEDULE GROUP BY SHIFT + // ======================================== + $this->db->select(' + sh.id as shift_id, + sh.shift_name, + + p.position_name, + + COUNT(s.id) as total_employee + '); + + $this->db->from('k_employee_shift_schedules s'); + + $this->db->join( + 'k_employees e', + 'e.id = s.employee_id', + 'left' + ); + + $this->db->join( + 'k_positions p', + 'p.id = e.position_id', + 'left' + ); + + $this->db->join( + 'k_shifts sh', + 'sh.id = s.shift_id', + 'left' + ); + + $this->db->where('DATE(s.start_datetime)', $date); + + // ======================================== + // GROUPING + // ======================================== + $this->db->group_by('sh.id'); + $this->db->group_by('p.id'); + + $this->db->order_by('sh.checkin_time','ASC'); + $this->db->order_by('p.position_name','ASC'); + + $rows = $this->db->get()->result(); + + // ======================================== + // FORMAT GROUPED + // ======================================== + $grouped = []; + + if($rows){ + + foreach($rows as $r){ + + $shiftId = $r->shift_id ?: 0; + + if(!isset($grouped[$shiftId])){ + + $grouped[$shiftId] = [ + + 'shift_id' => $r->shift_id, + + 'shift_name' => $r->shift_name ?: '-', + + 'positions' => [] + ]; + } + + $grouped[$shiftId]['positions'][] = [ + + 'position_name' => $r->position_name ?: '-', + + 'total' => (int)$r->total_employee + ]; + } + } + + $days[] = [ + + 'date' => date('d', strtotime($date)), + + 'month' => date('M', strtotime($date)), + + 'day_name' => $this->translateDay( + date('l', strtotime($date)) + ), + + 'full_date' => tanggal_indo($date), + + 'full_date_raw' => $date, + + 'is_current_month' => ( + date('m', strtotime($date)) + == + str_pad($month,2,'0',STR_PAD_LEFT) + ), + + 'shifts' => array_values($grouped) + ]; + + $current = strtotime('+1 day', $current); + } + + echo json_encode([ + 'days' => $days + ]); +} + + // ===================================================== + // GET EMPLOYEE + // ===================================================== + public function get_employees() + { + $position_id = $this->input->get('position_id'); + + $this->db->select(' + e.id, + e.full_name, + e.fingerprint_user_id, + + p.position_name + '); + + $this->db->from('k_employees e'); + + $this->db->join( + 'k_positions p', + 'p.id=e.position_id', + 'left' + ); + + $this->db->where('e.is_active',1); + + if(!empty($position_id)){ + $this->db->where('e.position_id',$position_id); + } + + $this->db->order_by('e.full_name','ASC'); + + $employees = $this->db->get()->result(); + + echo json_encode([ + 'data' => $employees + ]); + } + +// ===================================================== +// SAVE MANUAL SCHEDULE +// ===================================================== +public function save_manual_schedule() +{ + try{ + + $employee_ids = $this->input->post('employee_ids'); + + $schedule_date = $this->input->post('schedule_date'); + + $shift_id = $this->input->post('shift_id'); + + // ============================================ + // VALIDASI + // ============================================ + if(empty($employee_ids)){ + throw new Exception('Employee belum dipilih'); + } + + if(empty($schedule_date)){ + throw new Exception('Tanggal wajib diisi'); + } + + if(empty($shift_id)){ + throw new Exception('Shift wajib dipilih'); + } + + // ============================================ + // GET SHIFT + // ============================================ + $shift = $this->db + ->where('id', $shift_id) + ->get('k_shifts') + ->row(); + + if(!$shift){ + throw new Exception('Shift tidak ditemukan'); + } + + // ============================================ + // BUILD START DATETIME + // ============================================ + $start_datetime = date( + 'Y-m-d H:i:s', + strtotime( + $schedule_date . ' ' . $shift->checkin_time + ) + ); + + // ============================================ + // BUILD END DATETIME + // ============================================ + $end_datetime = date( + 'Y-m-d H:i:s', + strtotime( + $schedule_date . ' ' . $shift->checkout_time + ) + ); + + // ============================================ + // JIKA SHIFT NYEBRANG HARI + // ============================================ + $is_cross_day = 0; + + if( + strtotime($shift->checkout_time) + <= + strtotime($shift->checkin_time) + ){ + + $end_datetime = date( + 'Y-m-d H:i:s', + strtotime('+1 day', strtotime($end_datetime)) + ); + + $is_cross_day = 1; + } + + // ============================================ + // HITUNG TOTAL JAM KERJA + // ============================================ + $work_hours = ( + strtotime($end_datetime) + - + strtotime($start_datetime) + ) / 3600; + + // ============================================ + // LOOP EMPLOYEE + // ============================================ + foreach($employee_ids as $employee_id){ + + // ======================================== + // CHECK EXIST + // ======================================== + $exists = $this->db + ->where('employee_id', $employee_id) + ->where('schedule_date', $schedule_date) + ->get('k_employee_shift_schedules') + ->row(); + + // ======================================== + // DATA SAVE + // ======================================== + $saveData = [ + + 'employee_id' => $employee_id, + + 'shift_id' => $shift_id, + + 'start_datetime' => $start_datetime, + + 'end_datetime' => $end_datetime, + + 'work_hours' => $work_hours, + + 'is_cross_day' => $is_cross_day, + + 'schedule_source' => 'manual', + + 'notes' => 'manual planner', + + 'updated_at' => date('Y-m-d H:i:s') + ]; + + // ======================================== + // UPDATE + // ======================================== + if($exists){ + + $this->db + ->where('id', $exists->id) + ->update( + 'k_employee_shift_schedules', + $saveData + ); + + // ======================================== + // INSERT + // ======================================== + } else { + + $saveData['created_at'] = date('Y-m-d H:i:s'); + + $this->db->insert( + 'k_employee_shift_schedules', + $saveData + ); + } + } + + echo json_encode([ + 'status' => true, + 'message' => 'Jadwal berhasil disimpan' + ]); + + } catch(Exception $e){ + + echo json_encode([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } +} + + // ===================================================== + // TRANSLATE DAY + // ===================================================== + private function translateDay($day) + { + $days = [ + 'Sunday' => 'Minggu', + 'Monday' => 'Senin', + 'Tuesday' => 'Selasa', + 'Wednesday' => 'Rabu', + 'Thursday' => 'Kamis', + 'Friday' => 'Jumat', + 'Saturday' => 'Sabtu' + ]; + + return isset($days[$day]) + ? $days[$day] + : $day; + } + +// ===================================================== +// DETAIL SCHEDULE +// ===================================================== +public function detail_schedule() +{ + $schedule_date = $this->input->get('schedule_date'); + + if(empty($schedule_date)){ + + echo json_encode([ + 'status' => false, + 'message'=> 'Tanggal wajib diisi', + 'data' => [] + ]); + + return; + } + + // ============================================ + // GET DATA + // ============================================ + $this->db->select(' + s.id as schedule_id, + + s.employee_id, + s.shift_id, + + s.start_datetime, + s.end_datetime, + + e.employee_code, + e.full_name, + e.fingerprint_user_id, + + p.id as position_id, + p.position_name, + + sh.shift_name, + sh.checkin_time, + sh.checkout_time, + sh.color + '); + + $this->db->from('k_employee_shift_schedules s'); + + $this->db->join( + 'k_employees e', + 'e.id=s.employee_id', + 'left' + ); + + $this->db->join( + 'k_positions p', + 'p.id=e.position_id', + 'left' + ); + + $this->db->join( + 'k_shifts sh', + 'sh.id=s.shift_id', + 'left' + ); + + // ============================================ + // FILTER DATE + // ============================================ + $this->db->where( + 'DATE(s.start_datetime)', + $schedule_date + ); + + $this->db->order_by('p.position_name','ASC'); + + $this->db->order_by('e.full_name','ASC'); + + $rows = $this->db->get()->result(); + + // ============================================ + // GROUP POSITION + // ============================================ + $grouped = []; + + if($rows){ + + foreach($rows as $r){ + + $position_id = $r->position_id ?: 0; + + // ==================================== + // CREATE GROUP + // ==================================== + if(!isset($grouped[$position_id])){ + + $grouped[$position_id] = [ + + 'position_id' => $r->position_id, + + 'position_name' => $r->position_name ?: '-', + + 'employees' => [] + ]; + } + + // ==================================== + // EMPLOYEE + // ==================================== + $grouped[$position_id]['employees'][] = [ + + 'schedule_id' => $r->schedule_id, + + 'employee_id' => $r->employee_id, + + 'full_name' => $r->full_name, + + 'employee_code' => $r->employee_code, + + 'fingerprint_user_id' => $r->fingerprint_user_id, + + 'shift_id' => $r->shift_id, + + 'shift_name' => $r->shift_name, + + 'checkin' => $r->checkin_time + ? date( + 'H:i', + strtotime($r->checkin_time) + ) + : '-', + + 'checkout' => $r->checkout_time + ? date( + 'H:i', + strtotime($r->checkout_time) + ) + : '-', + + 'start_datetime' => $r->start_datetime, + + 'end_datetime' => $r->end_datetime, + + 'color' => $r->color + ]; + } + } + + echo json_encode([ + 'status' => true, + 'data' => array_values($grouped) + ]); +} + +// ===================================================== +// UPDATE SCHEDULE SHIFT +// ===================================================== +public function update_schedule_shift() +{ + try{ + + $schedule_id = $this->input->post('schedule_id'); + + $shift_id = $this->input->post('shift_id'); + + // ============================================ + // VALIDASI + // ============================================ + if(empty($schedule_id)){ + throw new Exception('Schedule ID kosong'); + } + + if(empty($shift_id)){ + throw new Exception('Shift wajib dipilih'); + } + + // ============================================ + // CHECK SCHEDULE + // ============================================ + $schedule = $this->db + ->where('id', $schedule_id) + ->get('k_employee_shift_schedules') + ->row(); + + if(!$schedule){ + throw new Exception('Schedule tidak ditemukan'); + } + + // ============================================ + // CHECK SHIFT + // ============================================ + $shift = $this->db + ->where('id', $shift_id) + ->get('k_shifts') + ->row(); + + if(!$shift){ + throw new Exception('Shift tidak ditemukan'); + } + + // ============================================ + // AMBIL TANGGAL + // ============================================ + $schedule_date = date( + 'Y-m-d', + strtotime($schedule->start_datetime) + ); + + // ============================================ + // BUILD START DATETIME + // ============================================ + $start_datetime = date( + 'Y-m-d H:i:s', + strtotime( + $schedule_date . ' ' . $shift->checkin_time + ) + ); + + // ============================================ + // BUILD END DATETIME + // ============================================ + $end_datetime = date( + 'Y-m-d H:i:s', + strtotime( + $schedule_date . ' ' . $shift->checkout_time + ) + ); + + // ============================================ + // CROSS DAY + // ============================================ + $is_cross_day = 0; + + if( + strtotime($shift->checkout_time) + <= + strtotime($shift->checkin_time) + ){ + + $end_datetime = date( + 'Y-m-d H:i:s', + strtotime('+1 day', strtotime($end_datetime)) + ); + + $is_cross_day = 1; + } + + // ============================================ + // WORK HOURS + // ============================================ + $work_hours = $shift->work_hours; + + // ============================================ + // UPDATE + // ============================================ + $this->db + ->where('id', $schedule_id) + ->update( + 'k_employee_shift_schedules', + [ + + 'shift_id' => $shift_id, + + 'start_datetime' => $start_datetime, + + 'end_datetime' => $end_datetime, + + 'work_hours' => $work_hours, + + 'is_cross_day' => $is_cross_day, + + 'updated_at' => date('Y-m-d H:i:s') + ] + ); + + echo json_encode([ + 'status' => true, + 'message' => 'Shift berhasil diupdate' + ]); + + } catch(Exception $e){ + + echo json_encode([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } +} + + +// ===================================================== +// DELETE SCHEDULE +// ===================================================== +public function delete_schedule() +{ + try{ + + $schedule_id = $this->input->post('schedule_id'); + + // ============================================ + // VALIDASI + // ============================================ + if(empty($schedule_id)){ + throw new Exception('Schedule ID kosong'); + } + + // ============================================ + // CHECK DATA + // ============================================ + $schedule = $this->db + ->where('id', $schedule_id) + ->get('k_employee_shift_schedules') + ->row(); + + if(!$schedule){ + throw new Exception('Schedule tidak ditemukan'); + } + + // ============================================ + // DELETE + // ============================================ + $this->db + ->where('id', $schedule_id) + ->delete('k_employee_shift_schedules'); + + echo json_encode([ + 'status' => true, + 'message' => 'Schedule berhasil dihapus' + ]); + + } catch(Exception $e){ + + echo json_encode([ + 'status' => false, + 'message' => $e->getMessage() + ]); + } +} + +} \ No newline at end of file diff --git a/application/controllers/Holidays.php b/application/controllers/Holidays.php new file mode 100644 index 0000000..45ae11d --- /dev/null +++ b/application/controllers/Holidays.php @@ -0,0 +1,177 @@ +load->database(); + } + + // ========================================= + // VIEW + // ========================================= + public function index() + { + $data = [ + "active_menu" => "holidays" + ]; + + $this->load->view('partials/header', $data); + $this->load->view('employees/holidays'); + $this->load->view('partials/footer'); + } + + // ========================================= + // GET DATA + // ========================================= + public function get_data() + { + $data = $this->db + ->order_by('holiday_date','DESC') + ->get('k_holidays') + ->result(); + + $rows = []; + + $no = 1; + + foreach($data as $d){ + + $rows[] = [ + $no++, + date('d M Y', strtotime($d->holiday_date)), + $d->holiday_name, + $d->is_national == 1 + ? 'Nasional' + : 'Perusahaan', + ' + + + + ' + ]; + } + + echo json_encode([ + 'data' => $rows + ]); + } + + // ========================================= + // DETAIL + // ========================================= + public function detail($id) + { + $data = $this->db + ->get_where('k_holidays',['id'=>$id]) + ->row(); + + echo json_encode($data); + } + + // ========================================= + // SAVE + // ========================================= + public function save() + { + $holiday_date = $this->input->post('holiday_date'); + $holiday_name = $this->input->post('holiday_name'); + $is_national = $this->input->post('is_national'); + + $cek = $this->db + ->get_where('k_holidays',[ + 'holiday_date' => $holiday_date + ]) + ->row(); + + if($cek){ + + echo json_encode([ + 'status' => false, + 'message' => 'Tanggal holiday sudah ada' + ]); + + return; + } + + $insert = [ + 'holiday_date' => $holiday_date, + 'holiday_name' => $holiday_name, + 'is_national' => $is_national + ]; + + $this->db->insert('k_holidays',$insert); + + echo json_encode([ + 'status' => true, + 'message' => 'Holiday berhasil ditambahkan' + ]); + } + + // ========================================= + // UPDATE + // ========================================= + public function update() + { + $id = $this->input->post('id'); + $holiday_date = $this->input->post('holiday_date'); + $holiday_name = $this->input->post('holiday_name'); + $is_national = $this->input->post('is_national'); + + $cek = $this->db + ->where('holiday_date',$holiday_date) + ->where('id !=',$id) + ->get('k_holidays') + ->row(); + + if($cek){ + + echo json_encode([ + 'status' => false, + 'message' => 'Tanggal holiday sudah ada' + ]); + + return; + } + + $update = [ + 'holiday_date' => $holiday_date, + 'holiday_name' => $holiday_name, + 'is_national' => $is_national + ]; + + $this->db + ->where('id',$id) + ->update('k_holidays',$update); + + echo json_encode([ + 'status' => true, + 'message' => 'Holiday berhasil diupdate' + ]); + } + + // ========================================= + // DELETE + // ========================================= + public function delete($id) + { + $this->db + ->where('id',$id) + ->delete('k_holidays'); + + echo json_encode([ + 'status' => true, + 'message' => 'Holiday berhasil dihapus' + ]); + } + +} \ No newline at end of file diff --git a/application/controllers/Invoices.php b/application/controllers/Invoices.php new file mode 100644 index 0000000..4a462f3 --- /dev/null +++ b/application/controllers/Invoices.php @@ -0,0 +1,1692 @@ +load->database(); + + $this->load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + // ========================= + // VIEW + // ========================= + + public function paid() + { + $data = ["active_menu" => "invoice_paid"]; + $this->load->view('partials/header', $data); + $this->load->view('invoices/layout'); + $this->load->view('partials/footer'); + } + + public function draft() + { + $data = ["active_menu" => "invoice_draft"]; + $this->load->view('partials/header', $data); + $this->load->view('invoices/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET DATA (FILTER STATUS) + // ========================= + + public function get_data($status = null) + { + $this->db->select(' + invoices.id, + invoices.customer_id, + invoices.no_invoice, + invoices.tanggal, + invoices.jatuh_tempo, + invoices.total, + invoices.total_bayar, + invoices.sisa_piutang, + invoices.status, + customers.nama + '); + $this->db->from('invoices'); + $this->db->join('customers','customers.id = invoices.customer_id'); + + // ========================= + // FILTER STATUS + // ========================= + if ($status) { + if($status == 'draft'){ + $this->db->where_in('invoices.status', ['unpaid','partial','draft']); + } else { + $this->db->where('invoices.status', $status); + } + } + + $this->db->where('invoices.deleted_at IS NULL', null, false); + $this->db->order_by('invoices.tanggal','DESC'); + $this->db->order_by('customers.nama','ASC'); + + $data = $this->db->get()->result(); + + $result = []; + $no = 1; + + foreach ($data as $row) { + + // ========================= + // CEK TUNGGAKAN SEBELUMNYA + // ========================= + $this->db->select_sum('sisa_piutang'); + $this->db->where('customer_id', $row->customer_id); + $this->db->where('status !=', 'paid'); + $this->db->where('id <', $row->id); + + $tunggakan = $this->db->get('invoices')->row()->sisa_piutang ?? 0; + + // ========================= + // STATUS BADGE + // ========================= + switch ($row->status) { + + case 'paid': + $status_badge = 'PAID'; + break; + + case 'partial': + $status_badge = 'PARTIAL'; + break; + + case 'unpaid': + $status_badge = 'UNPAID'; + break; + + default: + $status_badge = 'DRAFT'; + } + + // ========================= + // AKSI + // ========================= + $aksi = ''; + + // tombol detail + $aksi .= ' '; + + // ========================= + // LOGIC BAYAR + // ========================= + $bolehBayar = ($tunggakan <= 0); + + // DRAFT + if($row->status == 'draft'){ + + if ($row->total > 0 && $bolehBayar) { + $aksi .= ' '; + } + + $aksi .= ''; + } + + // UNPAID + PARTIAL + if(in_array($row->status, ['unpaid','partial'])){ + + if($bolehBayar){ + $aksi .= ' '; + } else { + // indikator tunggakan + $aksi .= ' + Ada tunggakan + '; + } + } + + // ========================= + // FORMAT OUTPUT + // ========================= + $result[] = [ + $no++, + ''.$row->no_invoice.'', + tanggal_indo($row->tanggal), + tanggal_indo($row->jatuh_tempo), + $row->nama, + 'Rp '.number_format($row->total,0,',','.').'', + 'Rp '.number_format($row->total_bayar,0,',','.').'', + 'Rp '.number_format($row->sisa_piutang,0,',','.').'', + $status_badge, + $aksi + ]; + } + + echo json_encode(["data"=>$result]); + } + + // ========================= + // GET CUSTOMER + // ========================= + public function get_customers() + { + echo json_encode($this->db->get('customers')->result()); + } + + // ========================= + // SAVE INVOICE + // ========================= + public function save() + { + $this->output->set_content_type('application/json'); + + $customer_id = $this->input->post('customer_id'); + $tanggal = $this->input->post('tanggal'); + $jatuh_tempo = $this->input->post('jatuh_tempo'); + + // ========================= + // VALIDASI WAJIB FIELD + // ========================= + if (empty($customer_id) || empty($tanggal) || empty($jatuh_tempo)) { + echo json_encode([ + 'status' => false, + 'message' => 'Customer, tanggal, dan jatuh tempo wajib diisi' + ]); + return; + } + + // ========================= + // NORMALISASI TANGGAL + // ========================= + $today = date('Y-m-d'); + $tanggal = date('Y-m-d', strtotime($tanggal)); + $jatuh_tempo = date('Y-m-d', strtotime($jatuh_tempo)); + + + // ❌ jatuh tempo tidak boleh kurang dari tanggal invoice + if ($jatuh_tempo < $tanggal) { + echo json_encode([ + 'status' => false, + 'message' => 'Jatuh tempo tidak boleh lebih kecil dari tanggal invoice' + ]); + return; + } + + // ========================= + // GENERATE NO INVOICE + // ========================= + $tanggal_now = date('Ymd'); + + $this->db->like('no_invoice', 'INV-'.$tanggal_now, 'after'); + $this->db->from('invoices'); + $count = $this->db->count_all_results(); + + $urutan = str_pad($count + 1, 4, '0', STR_PAD_LEFT); + + $no_invoice = 'INV-' . $tanggal_now . '-' . $urutan; + + // ========================= + // HANDEL Adb->query(" + SELECT * + FROM invoice_installments + WHERE customer_id = ? + AND status IN ('unpaid','partial') + AND (nominal - paid_amount) > 0 + AND is_billed = 0 + ORDER BY invoice_detail_id, angsuran_ke ASC + ", [$customer_id])->result(); + + $cicilan = []; + $group = []; + + foreach ($cicilan_raw as $c) { + $group[$c->invoice_detail_id][] = $c; + } + + foreach ($group as $detail_id => $rows) { + + $picked_status = []; + $min_row = null; + + foreach ($rows as $r) { + + // ambil angsuran terkecil + if ($min_row === null) { + $min_row = $r; + $picked_status[$r->status] = true; + $cicilan[] = $r; + continue; + } + + // ambil jika status beda dan belum pernah diambil + if (!isset($picked_status[$r->status])) { + $picked_status[$r->status] = true; + $cicilan[] = $r; + } + } + } + + // ========================= + // TRANSACTION + // ========================= + $this->db->trans_start(); + + $this->db->insert('invoices', [ + 'no_invoice' => $no_invoice, + 'customer_id' => $customer_id, + 'tanggal' => $tanggal, + 'jatuh_tempo' => $jatuh_tempo, + 'total' => 0, + 'status' => 'draft' + ]); + + $invoice_id = $this->db->insert_id(); + + $total_invoice = 0; + + foreach ($cicilan as $c) { + + // ambil data item asli + $inv_items = $this->db + ->select('account_id, items_id, nama_item, keterangan, subtotal_asli, is_cicilan') + ->where('id', $c->invoice_detail_id) + ->get('invoice_details') + ->row(); + + if(!$inv_items) continue; + + // ========================= + // FORMAT NAMA + // ========================= + if($c->paid_amount > 0){ + $priodecicilan = '( Sisa Cicilan ke ' . $c->angsuran_ke . '/' . $c->total_angsuran . ' )'; + }else{ + $priodecicilan = '( Cicilan ke ' . $c->angsuran_ke . '/' . $c->total_angsuran . ' )'; + } + + $nama_item = preg_replace( + '/\s*\(\s*Cicilan.*$/i', + '', + $inv_items->nama_item + ) . ' ' . $priodecicilan; + + // ========================= + // NOMINAL + // ========================= + $harga_cicilan = max(0, $c->nominal - $c->paid_amount); + if($harga_cicilan <= 0) continue; + + // ========================= + // INSERT DETAIL + // ========================= + $insert_data = [ + 'invoice_id' => $invoice_id, + 'tanggal' => $tanggal, + 'account_id' => $inv_items->account_id, + 'items_id' => $inv_items->items_id ?: null, + 'nama_item' => $nama_item, + 'qty' => 1, + 'harga' => $harga_cicilan, + 'subtotal' => $harga_cicilan, + 'warehouse_id' => null, + 'keterangan' => $inv_items->keterangan, + 'subtotal_asli' => $inv_items->subtotal_asli, + 'is_cicilan' => $inv_items->is_cicilan + ]; + + $this->db->insert('invoice_details', $insert_data); + + // ========================= + // AKUMULASI TOTAL + // ========================= + $total_invoice += $harga_cicilan; + + // ========================= + // MARK SUDAH DITAGIHKAN + // ========================= + $this->db->where('id', $c->id)->update('invoice_installments', [ + 'is_billed' => 1 + ]); + } + + // ========================= + // UPDATE TOTAL INVOICE + // ========================= + $this->update_total($invoice_id); + + $this->db->trans_complete(); + + // ========================= + // RESPONSE + // ========================= + if (!$this->db->trans_status()) { + echo json_encode([ + 'status' => false, + 'message' => 'Gagal membuat invoice' + ]); + return; + } + + log_activity( + 'invoice', + 'create', + 'Membuat invoice ' . $no_invoice . ' untuk customer ID ' . $customer_id, + 'success' + ); + + echo json_encode([ + 'status' => true, + 'invoice_id' => $invoice_id, + 'no_invoice' => $no_invoice + ]); + } + + public function bayar() + { + $this->output->set_content_type('application/json'); + + $invoice_id = $this->input->post('invoice_id'); + $tanggal_bayar = $this->input->post('tanggal_bayar'); + $detail = $this->input->post('detail'); + + if(!$invoice_id || empty($detail)){ + echo json_encode(['status'=>false,'message'=>'Data tidak valid']); + return; + } + + $invoice = $this->db->get_where('invoices',['id'=>$invoice_id])->row(); + if(!$invoice){ + echo json_encode(['status'=>false,'message'=>'Invoice tidak ditemukan']); + return; + } + + $customer = $this->db->get_where('customers',['id'=>$invoice->customer_id])->row(); + if(!$customer){ + echo json_encode(['status'=>false,'message'=>'Customer tidak ditemukan']); + return; + } + + // ========================= + // HITUNG TOTAL BAYAR + // ========================= + $total_bayar_input = 0; + foreach ($detail as $d) { + $total_bayar_input += (float)$d['amount']; + } + + if($total_bayar_input <= 0){ + echo json_encode(['status'=>false,'message'=>'Total bayar harus > 0']); + return; + } + + // ========================= + // DATA INVOICE + // ========================= + $paid_before = (float)$invoice->total_bayar; + $total_invoice = (float)$invoice->total; + + $new_total_bayar = $paid_before + $total_bayar_input; + $sisa_setelah = $total_invoice - $new_total_bayar; + + if($new_total_bayar > $total_invoice){ + echo json_encode([ + 'status'=>false, + 'message'=>'Pembayaran melebihi total invoice' + ]); + return; + } + + // ========================= + // AKUN + // ========================= + $akun_tabungan = 70; + $akun_barang = 62; + $akun_piutang = 19; + + // ========================= + // HITUNG TOTAL CICILAN + // ========================= + + $cicilan = $this->db->query(" + SELECT ii.* + FROM invoice_installments ii + JOIN ( + SELECT invoice_detail_id, MIN(angsuran_ke) as angsuran_ke + FROM invoice_installments + WHERE customer_id = ? + AND status != 'paid' + GROUP BY invoice_detail_id + ) x + ON x.invoice_detail_id = ii.invoice_detail_id + AND x.angsuran_ke = ii.angsuran_ke + ", [$customer->id])->result(); + + $total_cicilan = 0; + + foreach($cicilan as $c){ + $total_cicilan += ($c->nominal - $c->paid_amount); + } + + // ========================= + // TOTAL PER KATEGORI + // ========================= + $total_tabungan = (float) $this->db + ->select('SUM(subtotal) as total') + ->where('invoice_id', $invoice_id) + ->where('account_id', $akun_tabungan) + ->get('invoice_details')->row()->total ?? 0; + + $total_barang = (float) $this->db + ->select('SUM(subtotal) as total') + ->where('invoice_id', $invoice_id) + ->where('account_id', $akun_barang) + ->get('invoice_details')->row()->total ?? 0; + + $total_other = (float) $this->db + ->select('SUM(subtotal) as total') + ->where('invoice_id', $invoice_id) + ->where_not_in('account_id', [$akun_tabungan, $akun_barang]) + ->get('invoice_details')->row()->total ?? 0; + + // ========================= + // SISA PER KATEGORI + // ========================= + $sisa_other = max(0, $total_other - $paid_before); + $sisa_tabungan = max(0, $total_tabungan - max(0, $paid_before - $total_other)); + $sisa_barang = max(0, $total_barang - max(0, $paid_before - $total_other - $total_tabungan)); + + // ========================= + // ALOKASI + // ========================= + $sisa_bayar = $total_bayar_input; + + // ========================= + // 1. CICILAN (PER ITEM) + // ========================= + $bayar_cicilan = min($sisa_bayar, $total_cicilan); + $sisa_bayar -= $bayar_cicilan; + + // ========================= + // 2. OTHER + // ========================= + $bayar_other = min($sisa_bayar, $sisa_other); + $sisa_bayar -= $bayar_other; + + // ========================= + // 3. TABUNGAN + // ========================= + $bayar_tabungan = min($sisa_bayar, $sisa_tabungan); + $sisa_bayar -= $bayar_tabungan; + + // ========================= + // 4. BARANG + // ========================= + $bayar_barang = min($sisa_bayar, $sisa_barang); + $sisa_bayar -= $bayar_barang; + + // ========================= + // TRANSACTION + // ========================= + $this->db->trans_start(); + + // UPDATE invoice_installments jika perlu di update + if($bayar_cicilan > 0){ + + $sisa = $bayar_cicilan; // ✅ FIX + + foreach($cicilan as $c){ + + if($sisa <= 0) break; + + $kurang = $c->nominal - $c->paid_amount; + $bayar = min($sisa, $kurang); + + $sisa -= $bayar; + + $new_paid = $c->paid_amount + $bayar; + $status = ($new_paid >= $c->nominal) ? 'paid' : 'partial'; + + $this->db->where('id',$c->id)->update('invoice_installments',[ + 'paid_amount' => $new_paid, + 'status' => $status + ]); + } + } + + $no_ref = 'PAY-' . date('YmdHis'); + + $this->db->insert('journals', [ + 'tanggal' => $tanggal_bayar, + 'no_ref' => $no_ref, + 'keterangan'=> 'Diterima Pembayaran Invoice '.$invoice->no_invoice.' ('.$customer->nama.')', + 'ref_type' => 'invoice_payment', + 'ref_id' => $invoice_id, + 'created_by'=> $this->session->userdata('user_id') + ]); + + $journal_id = $this->db->insert_id(); + + // DEBIT + $total_debit = 0; + foreach ($detail as $d) { + if(empty($d['account_id']) || $d['amount'] <= 0) continue; + + $this->db->insert('journal_details', [ + 'journal_id' => $journal_id, + 'account_id' => $d['account_id'], + 'debit' => $d['amount'], + 'kredit' => 0 + ]); + + $total_debit += $d['amount']; + } + + // ========================= + // KREDIT + // ========================= + $total_kredit = 0; + + $kredit_piutang = $bayar_cicilan + $bayar_other + $bayar_barang; + + if($kredit_piutang > 0){ + $this->db->insert('journal_details', [ + 'journal_id'=>$journal_id, + 'account_id'=>$akun_piutang, + 'debit'=>0, + 'kredit'=>$kredit_piutang + ]); + $total_kredit += $kredit_piutang; + } + + if($bayar_tabungan > 0){ + $this->db->insert('journal_details', [ + 'journal_id'=>$journal_id, + 'account_id'=>$akun_tabungan, + 'debit'=>0, + 'kredit'=>$bayar_tabungan + ]); + $total_kredit += $bayar_tabungan; + } + + // VALIDASI + if(round($total_debit,2) !== round($total_kredit,2)){ + $this->db->trans_rollback(); + echo json_encode([ + 'status'=>false, + 'message'=>'Jurnal tidak balance' + ]); + return; + } + + // PAYMENT + $this->db->insert('payments', [ + 'invoice_id'=>$invoice_id, + 'tanggal'=>$tanggal_bayar, + 'jumlah'=>$total_bayar_input, + 'metode'=>'manual', + 'journal_id'=>$journal_id + ]); + + // STATUS + if($new_total_bayar >= $total_invoice){ + $status = 'paid'; + $sisa_setelah = 0; + }elseif($new_total_bayar > 0){ + $status = 'partial'; + }else{ + $status = 'unpaid'; + } + + $this->db->where('id',$invoice_id)->update('invoices',[ + 'total_bayar'=>$new_total_bayar, + 'sisa_piutang'=>$sisa_setelah, + 'status'=>$status, + 'updated_at'=>date('Y-m-d H:i:s'), + 'updated_by'=>$this->session->userdata('user_id') + ]); + + $this->db->trans_complete(); + + // ========================= + // RESPONSE + // ========================= + if (!$this->db->trans_status()) { + echo json_encode([ + 'status' => false, + 'message' => 'Gagal menyelesaikan pembayaran' + ]); + return; + } + + log_activity( + 'invoice', + 'create', + 'Melakukan pembayaran invoice ' . $invoice->no_invoice . ' untuk customer a.n ' . $customer->nama, + 'success' + ); + + echo json_encode([ + 'status'=>true, + 'message'=>'Pembayaran berhasil', + 'sisa'=>$sisa_setelah + ]); + } + + // ========================= + // DELETE + // ========================= + public function delete($id) + { + // Ambil invoice + $invoice = $this->db->get_where('invoices', ['id' => $id])->row(); + + if (!$invoice) { + echo json_encode(['status'=>false,'message'=>'Invoice tidak ditemukan']); + return; + } + + // ❌ Cek detail + $cekDetail = $this->db->where('invoice_id', $id) + ->count_all_results('invoice_details'); + + if ($cekDetail > 0) { + echo json_encode([ + 'status'=>false, + 'message'=>'Invoice tidak bisa dihapus karena masih memiliki detail' + ]); + return; + } + + if ($invoice->posted == 1) { + echo json_encode([ + 'status'=>false, + 'message'=>'Invoice sudah diposting, tidak bisa dihapus' + ]); + return; + } + + // ❌ Cek sudah dibayar + if (in_array($invoice->status, ['paid','partial'])) { + echo json_encode([ + 'status'=>false, + 'message'=>'Invoice sudah ada pembayaran, tidak bisa dihapus' + ]); + return; + } + + // ✅ Hapus + $this->db->delete('invoices', ['id'=>$id]); + + log_activity( + 'invoice', + 'delete', + 'Menghapus invoice ID ' . $id . ' - ' . $invoice->no_invoice, + 'success' + ); + + echo json_encode(['status'=>true]); + } + + + public function detail($id) + { + // GET customer_id + $row = $this->db->select('customer_id') + ->where('id',$id) + ->get('invoices') + ->row(); + + $data['active_menu'] = "invoice_draft"; + $data['invoice_id'] = $id; + $data['customer_id'] = $row ? $row->customer_id : null; + + $this->load->view('partials/header', $data); + $this->load->view('invoices/detail', $data); + $this->load->view('partials/footer'); + } + + + public function get_invoice($id) + { + // ================= AMBIL INVOICE ================= + $this->db->select('invoices.*, customers.nama, customers.alamat, customers.telp, customers.email'); + $this->db->from('invoices'); + $this->db->join('customers', 'customers.id = invoices.customer_id'); + $this->db->where('invoices.id', $id); + + $data = $this->db->get()->row(); + + if(!$data){ + echo json_encode(null); + return; + } + + // ================= AMBIL TUNGGAKAN ================= + $this->db->select_sum('sisa_piutang'); + $this->db->where('customer_id', $data->customer_id); + $this->db->where('status !=', 'paid'); + $this->db->where('id !=', $id); // exclude invoice sekarang + + $tunggakan = $this->db->get('invoices')->row()->sisa_piutang ?? 0; + + // ================= MASUKKAN KE DATA ================= + $data->tunggakan = (int)$tunggakan; + + // ================= OUTPUT ================= + echo json_encode($data); + } + + // ========================= + // GET ITEMS + // ========================= + public function get_items($id) + { + $this->db->select('invoice_details.*, accounts.kode_akun, accounts.nama_akun, invoices.status'); + $this->db->from('invoice_details'); + $this->db->join('accounts', 'accounts.id = invoice_details.account_id', 'left'); + $this->db->join('invoices', 'invoices.id = invoice_details.invoice_id', 'left'); + $this->db->where('invoice_details.invoice_id', $id); + $this->db->order_by('tanggal','ASC'); + $this->db->order_by('id','ASC'); + + $data = $this->db->get()->result(); + + echo json_encode($data); + } + + // ========================= + // 🔥 CONFIG AKUN + // ========================= + private $akun_persediaan = 21; + private $akun_hpp = 63; + private $akun_piutang = 19; + + // ========================= + // ADD ITEM - FIXED RESPONSE + // ========================= + public function add_item() + { + $this->db->trans_begin(); + + $invoice_id = $this->input->post('invoice_id'); + $tanggal = $this->input->post('tanggal'); + $item_id = $this->input->post('item_id'); + $nama_item = $this->input->post('nama_item'); + $qty = (float)$this->input->post('qty'); + $harga = (float)$this->input->post('harga'); + $account_id = $this->input->post('account_id'); + $warehouse_id = $this->input->post('warehouse_id'); + + $total_harga = $qty * $harga; + $total_hpp = 0; + + // jika di bayar cicilan + $is_cicilan = $this->input->post('is_cicilan') ?? null; + $tenor = (int)$this->input->post('tenor') ?? 0; + + // ========================= + // VALIDASI & AMBIL ITEM + // ========================= + if($item_id){ + $item = $this->db->get_where('items', ['id'=>$item_id])->row(); + if(!$item){ + echo json_encode(['status'=>false, 'message'=>'Item tidak ditemukan']); + $this->db->trans_rollback(); + return; + } + + $nama_item = $item->nama_barang; + + // cek stok + $stok = $this->get_stok($item_id); + if($stok < $qty){ + echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup. Sisa: '.$stok]); + $this->db->trans_rollback(); + return; + } + + $total_hpp = $qty * $item->harga_beli; + } + + // ========================= + // HANDLE CICILAN + // ========================= + $harga_simpan = $harga; + $subtotal_simpan= $total_harga; + $harga_asli = $total_harga; + + $ket_cicilan_inv = ''; + $ket_cicilan_jr = ''; + + if ($is_cicilan && $tenor > 0) { + + // harga per cicilan + $harga_cicilan = round($total_harga / $tenor); + + $harga_simpan = $harga_cicilan; + $subtotal_simpan = $harga_cicilan; + + $ket_cicilan_inv = ' ( Cicilan ke 1/' . $tenor . ' )'; + $ket_cicilan_jr = ' ( Cicilan ' . $tenor . ' Periode )'; + } + + // ========================= + // INSERT DETAIL + // ========================= + $insert_data = [ + 'invoice_id' => $invoice_id, + 'tanggal' => $tanggal, + 'account_id' => $account_id, + 'items_id' => $item_id ?: null, + 'nama_item' => $nama_item . $ket_cicilan_inv, + 'qty' => $qty, + 'harga' => $harga_simpan, + 'subtotal' => $subtotal_simpan, + 'warehouse_id' => $warehouse_id, + 'keterangan' => $this->input->post('keterangan'), + 'subtotal_asli' => $harga_asli, + 'is_cicilan' => $is_cicilan ? 1 : 0 + ]; + + $this->db->insert('invoice_details', $insert_data); + $detail_id = $this->db->insert_id(); + + // ========================= + // JURNAL + STOK + // ========================= + if($this->input->post('account_id') != 70) { + $this->generate_jurnal_item($detail_id, $ket_cicilan_jr); + } + $this->update_total($invoice_id); + + // ambil customer dari invoice + $invoice = $this->db->get_where('invoices', ['id' => $invoice_id])->row(); + + if ($is_cicilan && $tenor > 0) { + + // generate cicilan + $this->generate_installments( + $invoice_id, + $detail_id, + $invoice->customer_id, + $total_harga, + $tenor, + $tanggal, + $account_id, + $item_id ?: null + ); + } + + // ✅ RESPONSE KONSISTEN DENGAN JS + if ($this->db->trans_status() === FALSE){ + $this->db->trans_rollback(); + echo json_encode(['status'=>false, 'message'=>'Transaksi gagal']); + } else { + $this->db->trans_commit(); + + log_activity( + 'invoice', + 'add_item', + 'Tambah item invoice ID ' . $invoice_id . ' - ' . $nama_item, + 'success' + ); + + echo json_encode(['status'=>true, 'message'=>'Item berhasil ditambahkan']); + } + } + + // ========================= + // UPDATE ITEM - RESPONSE KONSISTEN + // ========================= + public function update_item() + { + $this->db->trans_begin(); + + $id = $this->input->post('id'); + $old = $this->db->get_where('invoice_details', ['id'=>$id])->row(); + + if(!$old){ + echo json_encode(['status'=>false, 'message'=>'Item tidak ditemukan']); + return; + } + + $cek = $this->db->get_where('invoice_installments', [ + 'invoice_detail_id' => $id + ])->num_rows(); + + if ($cek > 0) { + echo json_encode([ + 'status' => false, + 'message' => 'Item cicilan tidak bisa diubah. Hapus dan buat ulang.' + ]); + return; + } + + // 🔥 REVERSE DATA LAMA + $this->delete_jurnal_item($id); + $this->delete_stok_item($id); + + // DATA BARU + $item_id = $this->input->post('item_id'); + $nama_item = $this->input->post('nama_item'); + $tanggal = $this->input->post('tanggal'); + + if($item_id){ + $item = $this->db->get_where('items', ['id'=>$item_id])->row(); + if($item){ + $nama_item = $item->nama_barang; + $stok = $this->get_stok($item_id); + $qty = (float)$this->input->post('qty'); + if($stok < $qty){ + echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup']); + return; + } + } + } + + $qty = (float)$this->input->post('qty'); + $harga = (float)$this->input->post('harga'); + + // UPDATE + $update_data = [ + 'tanggal' => $tanggal, + 'account_id' => $this->input->post('account_id'), + 'items_id' => $item_id ?: null, + 'nama_item' => $nama_item, + 'qty' => $qty, + 'harga' => $harga, + 'subtotal' => $qty * $harga, + 'warehouse_id' => $this->input->post('warehouse_id'), + 'keterangan' => $this->input->post('keterangan') + ]; + + $this->db->where('id', $id)->update('invoice_details', $update_data); + + // GENERATE ULANG + if($this->input->post('account_id') != 70) { + $this->generate_jurnal_item($id); + } + $this->update_total($old->invoice_id); + + // ✅ RESPONSE KONSISTEN DENGAN JS + if ($this->db->trans_status() === FALSE){ + $this->db->trans_rollback(); + echo json_encode(['status'=>false, 'message'=>'Update gagal']); + } else { + $this->db->trans_commit(); + + log_activity( + 'invoice', + 'update_item', + 'Update item invoice detail ID ' . $id, + 'success' + ); + + echo json_encode(['status'=>true, 'message'=>'Item berhasil diupdate']); + } + } + + // ========================= + // DELETE ITEM - RESPONSE KONSISTEN + // ========================= + public function delete_item($id) + { + $this->db->trans_begin(); + + $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); + + if(!$d){ + echo json_encode(['status'=>false, 'message'=>'Item tidak ditemukan']); + return; + } + + if($d->is_cicilan == 1) { + $this->delete_invoice_installments($id, $d->source_cicilan_id); + } + // reverse stok & hapus + $this->delete_jurnal_item($id); + $this->delete_stok_item($id); + $this->db->delete('invoice_details', ['id'=>$id]); + $this->update_total($d->invoice_id); + + // ✅ RESPONSE KONSISTEN DENGAN JS + if ($this->db->trans_status() === FALSE){ + $this->db->trans_rollback(); + echo json_encode(['status'=>false, 'message'=>'Hapus gagal']); + } else { + $this->db->trans_commit(); + + log_activity( + 'invoice', + 'delete_item', + 'Hapus item invoice detail ID ' . $id, + 'success' + ); + + echo json_encode(['status'=>true, 'message'=>'Item berhasil dihapus']); + } + } + + // ========================= + // AMBIL DATA CICILAN + // ========================= + public function get_cicilan($id) + { + $cicilan_raw = $this->db->query(" + SELECT + ii.*, + it.nama_barang + FROM invoice_installments ii + LEFT JOIN items it ON ii.item_id = it.id + WHERE ii.customer_id = ? + AND ii.status IN ('unpaid','partial') + AND (ii.nominal - ii.paid_amount) > 0 + AND ii.is_billed = 0 + ORDER BY ii.invoice_detail_id, ii.angsuran_ke ASC + ", [$id])->result(); + + echo json_encode($cicilan_raw); + } + + // public function save_cicilan() + // { + // $cicilan_ids = $this->input->post('cicilan_id'); + + // if (empty($cicilan_ids)) { + // echo json_encode([ + // 'status' => false, + // 'message' => 'Tidak ada data dipilih' + // ]); + // return; + // } + + // $this->db->trans_start(); + + // foreach ($cicilan_ids as $id) { + + // // ambil data cicilan + // $cicilan = $this->db->get_where('invoice_installments', [ + // 'id' => $id + // ])->row(); + + // if (!$cicilan) continue; + + // $sisa = $cicilan->nominal - $cicilan->paid_amount; + + // // insert ke invoice_details + // $this->db->insert('invoice_details', [ + // 'invoice_id' => $cicilan->invoice_id, + // 'customer_id' => $cicilan->customer_id, + // 'item_id' => $cicilan->item_id, + // 'qty' => 1, + // 'price' => $sisa, + // 'total' => $sisa, + // 'source_cicilan_id'=> $cicilan->id, // penting buat tracking + // 'created_at' => date('Y-m-d H:i:s') + // ]); + + // // update status cicilan jadi billed + // $this->db->update('invoice_installments', [ + // 'is_billed' => 1 + // ], ['id' => $id]); + // } + + // $this->db->trans_complete(); + + // echo json_encode([ + // 'status' => $this->db->trans_status() + // ]); + // } + // ========================= + // HAPUS HELPER LAMA - TIDAK DIPAKAI LAGI + // ========================= + // Hapus fungsi commit() & rollback() karena sudah inline + + private function generate_jurnal_item($id, $ket_cicilan_jr = null) + { + // $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); + + // if(!$d) return; + + $d = $this->db + ->select(' + invoice_details.*, + customers.nama, + invoices.no_invoice + ') + ->from('invoice_details') + ->join('invoices', 'invoices.id = invoice_details.invoice_id') + ->join('customers', 'customers.id = invoices.customer_id') + ->where('invoice_details.id', $id) + ->get() + ->row(); + + if(!$d) return; + + $total_hpp = 0; + + // ========================= + // 🔥 TENTUKAN NILAI JURNAL + // ========================= + $nilai_jurnal = $d->subtotal; + + if (isset($d->is_cicilan) && $d->is_cicilan == 1) { + $nilai_jurnal = $d->subtotal_asli ?? $d->subtotal; + } + + // ========================= + // 🔥 STOK & HPP + // ========================= + if($d->items_id){ + + $item = $this->db->get_where('items',['id'=>$d->items_id])->row(); + + if($item){ + $total_hpp = $item->harga_beli * $d->qty; + } + + // stok keluar + $this->db->insert('stock_logs', [ + 'item_id' => $d->items_id, + 'qty' => $d->qty, + 'tipe' => 'keluar', + 'ref_type' => 'invoice_details', + 'ref_id' => $id, + 'warehouse_id' => $d->warehouse_id, + 'keterangan' => 'Penjualan ' . $item->nama_barang . ' kepada ' . $d->nama . ' - Invoice ' . $d->no_invoice . ' - Tgl ' . $d->tanggal + ]); + + $this->update_item_stok($d->items_id); + } + + // ========================= + // 🔥 generate no_ref + // ========================= + $no_ref = $this->generate_no_ref(); + + // ========================= + // 🔥 JOURNAL HEADER + // ========================= + $nama_item_jr = preg_replace('/\s*\(\s*Cicilan.*$/i', '', $d->nama_item) . $ket_cicilan_jr; + $this->db->insert('journals', [ + 'tanggal' => $d->tanggal, + 'no_ref' => $no_ref, + 'keterangan' => 'Penjualan '.$nama_item_jr, + 'ref_type' => 'invoice_details', + 'ref_id' => $id, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + // ========================= + // 🔥 PIUTANG (DEBIT) + // ========================= + $this->db->insert('journal_details', [ + 'journal_id' => $jid, + 'account_id' => $this->akun_piutang, + 'debit' => $nilai_jurnal, + 'kredit' => 0 + ]); + + // ========================= + // 🔥 PENDAPATAN (KREDIT) + // ========================= + $this->db->insert('journal_details', [ + 'journal_id' => $jid, + 'account_id' => $d->account_id, + 'debit' => 0, + 'kredit' => $nilai_jurnal + ]); + + // ========================= + // 🔥 HPP & PERSEDIAAN + // ========================= + if($total_hpp > 0){ + + // HPP (debit) + $this->db->insert('journal_details', [ + 'journal_id' => $jid, + 'account_id' => $this->akun_hpp, + 'debit' => $total_hpp, + 'kredit' => 0 + ]); + + // Persediaan (kredit) + $this->db->insert('journal_details', [ + 'journal_id' => $jid, + 'account_id' => $this->akun_persediaan, + 'debit' => 0, + 'kredit' => $total_hpp + ]); + } + } + + // ========================= + // 🔥 DELETE JURNAL + // ========================= + private function delete_jurnal_item($id) + { + $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); + + $j = $this->db->get_where('journals', [ + 'ref_type'=>'invoice_details', + 'ref_id'=>$id + ])->result(); + + foreach($j as $row){ + $this->db->delete('journal_details', ['journal_id'=>$row->id]); + $this->db->delete('journals', ['id'=>$row->id]); + } + + if($d && $d->items_id){ + $this->update_item_stok($d->items_id); // ✅ BENAR + } + } + + // ========================= + // 🔥 DELETE STOK + // ========================= + private function delete_stok_item($id) + { + $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); + + $this->db->delete('stock_logs', [ + 'ref_type'=>'invoice_details', + 'ref_id'=>$id + ]); + + if($d && $d->items_id){ + $this->update_item_stok($d->items_id); // ✅ BENAR + } + } + + // ========================= + // 🔥 DELETE INVOICES INSTALLMENT + // ========================= + private function delete_invoice_installments($invoice_detail_id,$source_cicilan_id) + { + $cek = $this->db + ->where_in('status', ['paid', 'partial']) + ->where('invoice_detail_id', $invoice_detail_id) + ->count_all_results('invoice_installments'); + + // kalau TIDAK ADA paid/partial → hapus + if ($cek === 0) { + $this->db->delete('invoice_installments', [ + 'invoice_detail_id' => $invoice_detail_id + ]); + } + + if ($source_cicilan_id) { + $this->db->update('invoice_installments', [ + 'is_billed' => 0 + ], ['id' => $source_cicilan_id]); + } + } + + // ========================= + // 🔥 GET STOK + // ========================= + private function get_stok($item_id) + { + return $this->db->query(" + SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok + FROM stock_logs + WHERE item_id = ? + ", [$item_id])->row()->stok; + } + + // ========================= + // ADD ITEM - FIXED HELPER + // ========================= + private function commit() + { + if ($this->db->trans_status() === FALSE){ + $this->db->trans_rollback(); + echo json_encode(['status'=>false, 'message'=>'Transaksi gagal']); + } else { + $this->db->trans_commit(); + echo json_encode(['status'=>true, 'message'=>'Berhasil']); + } + } + + private function rollback($message) + { + $this->db->trans_rollback(); + echo json_encode(['status'=>false, 'message'=>$message]); + } + + // ========================= + // UPDATE TOTAL + // ========================= + private function update_total($invoice_id) + { + $this->db->select_sum('subtotal', 'total'); + $this->db->where('invoice_id', $invoice_id); + $total = $this->db->get('invoice_details')->row()->total ?? 0; + + $this->db->where('id', $invoice_id) + ->update('invoices', ['total' => $total]); + } + + // ========================= + // UPDATE TOTAL + // ========================= + private function generate_no_ref() + { + $tanggal = date('Y-m-d'); + $prefix = 'INV-'.date('Ymd').'-'; + + // ambil nomor terakhir hari ini + $last = $this->db->query(" + SELECT no_ref + FROM journals + WHERE DATE(tanggal) = ? + AND no_ref LIKE ? + ORDER BY no_ref DESC + LIMIT 1 + ", [$tanggal, $prefix.'%'])->row(); + + if($last){ + $last_number = (int) substr($last->no_ref, -4); + $next = $last_number + 1; + } else { + $next = 1; + } + + return $prefix . str_pad($next, 4, '0', STR_PAD_LEFT); + } + + // ========================= + // GET ACCOUNTS + // ========================= + public function get_accounts() + { + $data = $this->db + ->group_start() + ->where('tipe', 'revenue') + ->or_where('id', 70) + ->group_end() + ->order_by('kode_akun', 'ASC') + ->get('accounts') + ->result(); + + echo json_encode($data); + } + + public function get_accounts_assets() + { + $data = $this->db + ->where_in('kode_akun', [101,102,207,201,202,203,204,205]) + ->order_by('kode_akun', 'ASC') + ->get('accounts') + ->result(); + + echo json_encode($data); + } + + public function check_items_by_account($account_id) + { + $exists = $this->db + ->where('account_id', $account_id) + ->limit(1) + ->get('items') + ->num_rows() > 0; + + echo json_encode([ + 'has_item' => $exists + ]); + } + + public function get_items_by_wh_account($warehouse_id, $account_id) + { + $this->db->select(' + items.id, + items.nama_barang, + items.harga_jual, + items.kode_detail, + COALESCE(SUM( + stock_logs.qty * IF(stock_logs.tipe="masuk",1,-1) + ),0) as stok + '); + + $this->db->where('status', 'active'); + $this->db->from('items'); + + $this->db->join('stock_logs', + 'stock_logs.item_id = items.id + AND stock_logs.warehouse_id = '.$this->db->escape($warehouse_id), + 'left'); + + $this->db->where('items.account_id', $account_id); + + $this->db->group_by('items.id'); + + // ✅ INI KUNCINYA + $this->db->having('stok >', 0); + + + $data = $this->db->get()->result(); + + echo json_encode($data); + } + + public function gudang_list() + { + echo json_encode( + $this->db->get('warehouses')->result() + ); + } + + public function get_payment($invoice_id) + { + $this->db->select(' + id, + tanggal, + jumlah, + metode, + created_at + '); + $this->db->from('payments'); + $this->db->where('invoice_id', $invoice_id); + $this->db->order_by('id','DESC'); + + $data = $this->db->get()->result(); + + $result = []; + + $this->db->from('invoices'); + $this->db->where('invoices.id', $invoice_id); + + $invoice = $this->db->get()->row(); + + foreach ($data as $row) { + $result[] = [ + 'id' => $row->id, + 'tanggal' => date('d-m-Y', strtotime($row->tanggal)), + 'jumlah' => 'Rp '.number_format($row->jumlah,0,',','.'), + 'metode' => $row->metode, + 'created_at' => date('d-m-Y H:i', strtotime($row->created_at)) + ]; + } + + echo json_encode([ + 'data' => $result, + 'invoice' => $invoice + ]); + } + + // Update Stok Cache + private function update_item_stok($item_id) + { + $stok = $this->db->query(" + SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok + FROM stock_logs + WHERE item_id = ? + ", [$item_id])->row()->stok; + + $this->db->where('id', $item_id) + ->update('items', [ + 'stok' => $stok + ]); + } + + + private function generate_installments($invoice_id, $detail_id, $customer_id, $total, $tenor, $start_date, $account_id, $item_id = null) + { + $nominal = round($total / $tenor); + $sisa = $total - ($nominal * $tenor); + + for ($i = 1; $i <= $tenor; $i++) { + + $tgl = date('Y-m-d', strtotime("+".($i-1)." month", strtotime($start_date))); + + $nilai = $nominal; + + if ($i == $tenor) { + $nilai += $sisa; + } + + $is_billed = ($i == 1) ? 1 : 0; + + $this->db->insert('invoice_installments', [ + 'invoice_id' => $invoice_id, + 'invoice_detail_id' => $detail_id, // 🔥 relasi ke item + 'customer_id' => $customer_id, + 'angsuran_ke' => $i, + 'total_angsuran' => $tenor, + 'jatuh_tempo' => $tgl, + 'nominal' => $nilai, + 'account_id' => $account_id, + 'item_id' => $item_id ?: null, + 'status' => 'unpaid', + 'is_billed' => $is_billed + ]); + } + } + + public function save_cicilan() + { + $cicilan_ids = $this->input->post('cicilan_id'); + $invoice_id = $this->input->post('invoice_id'); + + if (empty($cicilan_ids)) { + echo json_encode([ + 'status' => false, + 'message' => 'Tidak ada data dipilih' + ]); + return; + } + + $this->db->trans_start(); + + foreach ($cicilan_ids as $id) { + + // ========================= + // AMBIL CICILAN + // ========================= + $cicilan = $this->db->get_where('invoice_installments', [ + 'id' => $id, + 'is_billed' => 0 // penting! + ])->row(); + + if (!$cicilan) continue; + + // ========================= + // CEK DUPLIKAT + // ========================= + $cek = $this->db->get_where('invoice_details', [ + 'source_cicilan_id' => $id + ])->row(); + + if ($cek) continue; + + $sisa = $cicilan->nominal - $cicilan->paid_amount; + + // ========================= + // AMBIL REFERENSI ITEM + // ========================= + $inv_items = $this->db + ->select('account_id, items_id, nama_item, keterangan, subtotal_asli, is_cicilan') + ->where('id', $cicilan->invoice_detail_id) + ->get('invoice_details') + ->row(); + + if(!$inv_items) continue; + + // ========================= + // FORMAT NAMA + // ========================= + if($cicilan->paid_amount > 0){ + $priodecicilan = '( Sisa Cicilan ke ' . $cicilan->angsuran_ke . '/' . $cicilan->total_angsuran . ' )'; + } else { + $priodecicilan = '( Cicilan ke ' . $cicilan->angsuran_ke . '/' . $cicilan->total_angsuran . ' )'; + } + + $nama_item = preg_replace( + '/\s*\(\s*Cicilan.*$/i', + '', + $inv_items->nama_item + ) . ' ' . $priodecicilan; + + // ========================= + // INSERT + // ========================= + $this->db->insert('invoice_details', [ + 'invoice_id' => $invoice_id, + 'tanggal' => $cicilan->jatuh_tempo, + 'account_id' => $inv_items->account_id, + 'items_id' => $inv_items->items_id ?: null, + 'nama_item' => $nama_item, + 'qty' => 1, + 'harga' => $sisa, + 'subtotal' => $sisa, + 'warehouse_id' => null, + 'keterangan' => $inv_items->keterangan, + 'subtotal_asli' => $inv_items->subtotal_asli, + 'is_cicilan' => $inv_items->is_cicilan, + 'source_cicilan_id' => $cicilan->id + ]); + + // ========================= + // UPDATE STATUS + // ========================= + $this->db->update('invoice_installments', [ + 'is_billed' => 1 + ], ['id' => $id]); + } + + $this->update_total($invoice_id); + + $this->db->trans_complete(); + + echo json_encode([ + 'status' => $this->db->trans_status() + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Items.php b/application/controllers/Items.php new file mode 100644 index 0000000..c1081f7 --- /dev/null +++ b/application/controllers/Items.php @@ -0,0 +1,795 @@ +session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + $data = ["active_menu" => "items"]; + $this->load->view('partials/header', $data); + $this->load->view('items/layout'); + $this->load->view('partials/footer'); + } + + public function draft_items() + { + $data = ["active_menu" => "draft_items"]; + $this->load->view('partials/header', $data); + $this->load->view('items/draft_items'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET DATA (FIX) + // ========================= + public function get_data() + { + $status = $this->input->get('s') ?? 'all'; + + $allowed = ['draft','active','all']; + if (!in_array($status, $allowed)) { + $status = 'all'; + } + + $this->db->select(' + items.id, + items.nama_barang, + items.kode_detail, + items.harga_beli, + items.harga_jual, + items.tanggal_pembelian, + items.created_at, + items.stok, + items.status, + items.kode_id, + ( + SELECT GROUP_CONCAT(DISTINCT w.nama SEPARATOR ", ") + FROM stock_logs sl + LEFT JOIN warehouses w ON sl.warehouse_id = w.id + WHERE sl.item_id = items.id + ) as gudang + '); + + if ($status !== 'all') { + $this->db->where('items.status', $status); + } + + // urutan custom + $this->db->order_by('(items.stok = 0)', 'ASC', false); + $this->db->order_by('items.tanggal_pembelian', 'ASC'); + + $this->db->from('items'); + + $data = $this->db->get()->result(); + +// $this->db->select(' +// items.id, +// items.nama_barang, +// items.kode_detail, +// items.harga_beli, +// items.harga_jual, +// items.tanggal_pembelian, +// items.created_at, +// items.stok, +// items.status, +// items.kode_id, +// ( +// SELECT GROUP_CONCAT(DISTINCT w.nama SEPARATOR ", ") +// FROM stock_logs sl +// LEFT JOIN warehouses w ON sl.warehouse_id = w.id +// WHERE sl.item_id = items.id +// ) as gudang +// '); + +// if ($status !== 'all') { +// $this->db->where('items.status', $status); +// } + +// // 🔥 filter utama per kode_id +// $this->db->where(" +// (items.stok > 0) +// OR +// ( +// items.stok = 0 +// AND items.tanggal_pembelian = ( +// SELECT MAX(i2.tanggal_pembelian) +// FROM items i2 +// WHERE i2.stok = 0 +// AND i2.kode_id = items.kode_id +// ) +// ) +// ", null, false); + +// // urutan +// $this->db->order_by('items.stok = 0', 'ASC', false); +// $this->db->order_by('items.tanggal_pembelian', 'ASC'); + +// $this->db->from('items'); + +// $data = $this->db->get()->result(); + + $result = []; + $no = 1; + + foreach($data as $row){ + + $btnDetail = ''; + $btnEdit = ''; + $btnDelete = ''; + + if($row->status == 'draft'){ + $btnPost = ''; + $action = $btnDetail . $btnPost . $btnDelete; + } else { + if($this->session->userdata('role') == 'Admin') { + $btnAdjust = ''; + } else { + $btnAdjust = ''; + } + $action = $btnDetail . $btnEdit . $btnAdjust . $btnDelete; + } + + $result[] = [ + $no++, + $row->tanggal_pembelian, + $row->kode_detail, + $row->nama_barang, + $row->gudang ?? '-', + $row->stok, + number_format($row->harga_beli ?? 0, 2), + number_format($row->harga_jual ?? 0, 2), + $row->created_at, + $action + ]; + } + + echo json_encode(["data"=>$result]); + } + + // ========================= + // DETAIL + // ========================= + public function detail_simple($id) + { + echo json_encode( + $this->db->get_where('items',['id'=>$id])->row() + ); + } + + // ========================= + // DETAIL + // ========================= + public function detail($id) + { + $item = $this->db->get_where('items',['id'=>$id])->row(); + + $logs = $this->db + ->select('stock_logs.*, warehouses.nama as gudang') + ->join('warehouses','warehouses.id = stock_logs.warehouse_id','left') + ->where('item_id',$id) + ->order_by('stock_logs.id','DESC') + ->get('stock_logs') + ->result(); + + echo json_encode([ + 'item'=>$item, + 'logs'=>$logs + ]); + } + + public function get_accounts() + { + $data = $this->db + ->where_in('kode_akun', [101,102,201]) + ->order_by('kode_akun','ASC') + ->get('accounts') + ->result(); + + echo json_encode($data); + } + + public function get_accounts_biaya() + { + $data = $this->db + ->where(['tipe' => 'expense', 'posisi'=> 'debit', 'kategori'=> 'laba_rugi']) + ->order_by('kode_akun','ASC') + ->get('accounts') + ->result(); + + echo json_encode($data); + } + + // ========================= + // SAVE (FIX TOTAL) + // ========================= + public function save() + { + $nama_barang = $this->input->post('nama_barang'); + $qty = (int)$this->input->post('qty'); + $harga_beli = (float)$this->input->post('harga_beli'); + $harga_jual = (float)$this->input->post('harga_jual'); + $warehouse_id = $this->input->post('warehouse_id'); + $account_kas = $this->input->post('account_kas'); + $kode_id = $this->input->post('kode_id'); + $tanggal_beli = $this->input->post('tanggal_beli'); + $status = $this->input->post('status'); + + // ================= VALIDASI ================= + if(!$nama_barang || $qty <= 0 || $harga_beli <= 0 || !$warehouse_id || !$account_kas){ + echo json_encode([ + 'status'=>false, + 'message'=>'Data tidak lengkap / tidak valid' + ]); + return; + } + + // ================= VALIDASI HARGA ================= + if($harga_beli > $harga_jual){ + echo json_encode([ + 'status'=>false, + 'message'=>'Harga beli tidak boleh lebih besar dari harga jual' + ]); + return; + } + + $kode = $this->db->get_where('kode_barang',['id'=>$kode_id])->row(); + + $tanggal_kode = date('dmy', strtotime($tanggal_beli)); + $kode_detail = $kode->kode_barang . '-' . $tanggal_kode; + + $total = $qty * $harga_beli; + + $this->db->trans_start(); + + // ================= INSERT ITEM ================= + $this->db->insert('items',[ + 'stok' =>$qty, + 'kode_detail' =>$kode_detail, + 'kode_id' =>$kode_id, + 'nama_barang' =>$nama_barang, + 'harga_beli' =>$harga_beli, + 'harga_jual' =>$harga_jual, + 'tanggal_pembelian' => $tanggal_beli, + 'status' => $status + ]); + + $item_id = $this->db->insert_id(); + + // ================= STOCK MASUK ================= + $this->db->insert('stock_logs',[ + 'item_id'=>$item_id, + 'warehouse_id'=>$warehouse_id, + 'qty'=>$qty, + 'tipe'=>'masuk', + 'keterangan'=>'Pembelian awal', + 'created_at'=>date('Y-m-d H:i:s') + ]); + + // ================= JURNAL ================= + $no_ref = 'BLI-'.date('Ymd').'-'.$item_id; + + $this->db->insert('journals',[ + 'tanggal'=>$tanggal_beli, + 'no_ref'=>$no_ref, + 'ref_type'=> 'new_items', + 'ref_id'=>$item_id, + 'keterangan'=>'Pembelian '.$nama_barang, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + // 🔥 Debit Persediaan + $this->db->insert('journal_details',[ + 'journal_id'=>$jid, + 'account_id'=>21, // persediaan + 'debit'=>$total, + 'kredit'=>0 + ]); + + // 🔥 Kredit Kas (DINAMIS) + $this->db->insert('journal_details',[ + 'journal_id'=>$jid, + 'account_id'=>$account_kas, + 'debit'=>0, + 'kredit'=>$total + ]); + + $this->db->trans_complete(); + + // ================= CEK TRANSAKSI ================= + if ($this->db->trans_status() === FALSE){ + echo json_encode([ + 'status'=>false, + 'message'=>'Gagal simpan data' + ]); + } else { + + log_activity( + 'items', + 'create', + 'Menambahkan item: ' . $nama_barang, + 'success' + ); + + echo json_encode([ + 'status'=>true, + 'message'=>'Berhasil tambah item' + ]); + } + } + + // ========================= + // UPDATE (FIX AKUN) + // ========================= + public function update() + { + $id = $this->input->post('id'); + $nama_barang = $this->input->post('nama_barang'); + $harga_baru = (float)$this->input->post('harga_beli'); + $harga_jual = (float)$this->input->post('harga_jual'); + + $item = $this->db->get_where('items',['id'=>$id])->row(); + + if(!$item){ + echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']); + return; + } + + $this->db->trans_start(); + + $this->db->where('id',$id)->update('items',[ + 'nama_barang'=>$nama_barang, + 'harga_beli'=>$harga_baru, + 'harga_jual'=>$harga_jual + ]); + + if($item->harga_beli != $harga_baru){ + + $stok = $this->db->select('SUM(qty * IF(tipe="masuk",1,-1)) as stok') + ->where('item_id',$id) + ->get('stock_logs') + ->row()->stok ?? 0; + + $selisih = ($harga_baru - $item->harga_beli) * $stok; + + if($selisih != 0){ + + $this->db->insert('journals',[ + 'tanggal'=>date('Y-m-d'), + 'no_ref'=>'ADJ-'.date('YmdHis').'-'.$id, + 'keterangan'=>'Penyesuaian harga '.$item->nama_barang, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + if($selisih > 0){ + // 🔥 naik → pendapatan + $this->db->insert_batch('journal_details',[ + [ + 'journal_id'=>$jid, + 'account_id'=>21, + 'debit'=>$selisih, + 'kredit'=>0 + ], + [ + 'journal_id'=>$jid, + 'account_id'=>65, // ✅ BENAR + 'debit'=>0, + 'kredit'=>$selisih + ] + ]); + }else{ + // 🔥 turun → beban + $this->db->insert_batch('journal_details',[ + [ + 'journal_id'=>$jid, + 'account_id'=>64, // ✅ BENAR + 'debit'=>abs($selisih), + 'kredit'=>0 + ], + [ + 'journal_id'=>$jid, + 'account_id'=>21, + 'debit'=>0, + 'kredit'=>abs($selisih) + ] + ]); + } + } + } + + $this->db->trans_complete(); + + log_activity( + 'items', + 'update', + 'Update item: ' . $nama_barang, + 'success' + ); + + $this->update_item_stok($id); + + echo json_encode(['status'=>true,'message'=>'Berhasil update + adjustment']); + } + + public function delete($id) + { + // cek item ada + $item = $this->db->get_where('items',['id'=>$id])->row(); + + if(!$item){ + echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']); + return; + } + + // cek ada transaksi stock + $cekStock = $this->db->where('item_id',$id)->count_all_results('stock_logs'); + + if($cekStock > 1){ + echo json_encode([ + 'status'=>false, + 'message'=>'Tidak bisa dihapus, sudah ada transaksi stok' + ]); + return; + } + + $this->db->trans_start(); + + // hapus item + $this->db->delete('stock_logs',['item_id'=>$id]); + $this->db->delete('items',['id'=>$id]); + + $j = $this->db->get_where('journals', [ + 'ref_type'=>'new_items', + 'ref_id'=>$id + ])->result(); + + foreach($j as $row){ + $this->db->delete('journal_details', ['journal_id'=>$row->id]); + $this->db->delete('journals', ['id'=>$row->id]); + } + + $this->db->trans_complete(); + + if ($this->db->trans_status() === FALSE){ + echo json_encode(['status'=>false,'message'=>'Gagal hapus']); + } else { + + log_activity( + 'items', + 'delete', + 'Hapus item ID: ' . $id, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Berhasil dihapus']); + } + } + + public function posting() + { + $id = $this->input->post('id'); + + if(!$id){ + echo json_encode([ + 'status' => false, + 'message' => 'ID tidak valid' + ]); + return; + } + + // cek item ada + $item = $this->db->get_where('items', ['id' => $id])->row(); + + if(!$item){ + echo json_encode([ + 'status' => false, + 'message' => 'Item tidak ditemukan' + ]); + return; + } + + // cegah double posting + if($item->status == 'active'){ + echo json_encode([ + 'status' => false, + 'message' => 'Barang sudah diterima sebelumnya' + ]); + return; + } + + $this->db->trans_start(); + + // update status saja + $this->db->where('id', $id); + $this->db->update('items', [ + 'status' => 'active' + ]); + + $this->db->trans_complete(); + + if ($this->db->trans_status() === FALSE){ + + echo json_encode([ + 'status' => false, + 'message' => 'Gagal memproses data' + ]); + + } else { + + // log aktivitas + log_activity( + 'items', + 'posting', + 'Barang diterima | ID: '.$id.' | Nama: '.$item->nama_barang, + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'Barang sudah diterima dan stok siap digunakan' + ]); + } + } + + // ========================= + // KELUAR BARANG (FIX TANPA stok column) + // ========================= + public function keluarkan() + { + $account_biaya = (int)$this->input->post('account_biaya'); + $tanggal_keluar = $this->input->post('tanggal_keluar'); + $warehouse_id_keluar = $this->input->post('warehouse_id_keluar'); + $barang_id = $this->input->post('barang_id'); + $qty_keluar = (int)$this->input->post('qty_keluar'); + $keterangan_keluar = $this->input->post('keterangan_keluar'); + + // ========================= + // VALIDASI + // ========================= + if(!$barang_id || !$warehouse_id_keluar || $qty_keluar <= 0){ + echo json_encode(['status'=>false,'message'=>'Data tidak valid']); + return; + } + + // ========================= + // CEK ITEM + // ========================= + $item = $this->db->get_where('items',['id'=>$barang_id])->row(); + if(!$item){ + echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']); + return; + } + + // ========================= + // HITUNG STOK + // ========================= + $stok = $this->db->select(' + COALESCE(SUM(qty * IF(tipe="masuk",1,-1)),0) as stok + ') + ->where('item_id',$barang_id) + ->where('warehouse_id',$warehouse_id_keluar) + ->get('stock_logs') + ->row()->stok; + + if($stok < $qty_keluar){ + echo json_encode(['status'=>false,'message'=>'Stok tidak cukup']); + return; + } + + $nilai = $qty_keluar * (float)$item->harga_beli; + + // ========================= + // TRANSACTION + // ========================= + $this->db->trans_start(); + + // log keluar + $this->db->insert('stock_logs',[ + 'item_id'=>$barang_id, + 'warehouse_id'=>$warehouse_id_keluar, + 'qty'=>$qty_keluar, + 'tipe'=>'keluar', + 'keterangan'=>$keterangan_keluar + ]); + + $stock_log_id = $this->db->insert_id(); + + // jurnal + $this->db->insert('journals',[ + 'tanggal'=>$tanggal_keluar, + 'no_ref'=>'GOUT-'.date('YmdHis').'-'.$barang_id, + 'keterangan'=>'Pengeluaran Barang '.$item->nama_barang . ' ( ' . $keterangan_keluar . ' )', + 'ref_type' =>'stock_logs', + 'ref_id' => $stock_log_id, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + // Debit biaya / HPP + $this->db->insert('journal_details',[ + 'journal_id'=>$jid, + 'account_id'=>$account_biaya, + 'debit'=>$nilai, + 'kredit'=>0 + ]); + + // Kredit persediaan + $this->db->insert('journal_details',[ + 'journal_id'=>$jid, + 'account_id'=>21, + 'debit'=>0, + 'kredit'=>$nilai + ]); + + $this->db->trans_complete(); + + // ========================= + // CEK TRANSACTION + // ========================= + if (!$this->db->trans_status()) { + echo json_encode(['status'=>false,'message'=>'Gagal proses']); + return; + } + + log_activity( + 'items', + 'stock_out', + 'Keluar barang: ' . $item->nama_barang . ' qty: ' . $qty_keluar . ' ( ' . $keterangan_keluar . ' )', + 'success' + ); + + $this->update_item_stok($barang_id); + + echo json_encode(['status'=>true,'message'=>'Barang berhasil dikeluarkan']); + } + + public function list() + { + echo json_encode( + $this->db->get('warehouses')->result() + ); + } + + + public function adjust() + { + $item_id = $this->input->post('item_id'); + $qty = (int)$this->input->post('qty'); + $warehouse_id = $this->input->post('warehouse_id'); + $ket = $this->input->post('keterangan'); + + if($qty == 0){ + echo json_encode(['status'=>false,'message'=>'Qty tidak boleh 0']); + return; + } + + $item = $this->db->get_where('items',['id'=>$item_id])->row(); + + $nilai = abs($qty) * $item->harga_beli; + + $this->db->trans_start(); + + // 🔥 stock log + $this->db->insert('stock_logs',[ + 'item_id'=>$item_id, + 'warehouse_id'=>$warehouse_id, + 'qty'=>abs($qty), + 'tipe'=> $qty > 0 ? 'masuk' : 'keluar', + 'keterangan'=>'Adjustment: '.$ket + ]); + + // 🔥 jurnal + $this->db->insert('journals',[ + 'tanggal'=>date('Y-m-d'), + 'no_ref'=>'ADJ-'.date('Ymd').'-'.$item_id, + 'keterangan'=>'Penyesuaian '.$item->nama_barang, + 'created_by' => $this->session->userdata('user_id') + ]); + + $jid = $this->db->insert_id(); + + if($qty > 0){ + // stok naik + $this->db->insert_batch('journal_details',[ + [ + 'journal_id'=>$jid, + 'account_id'=>21, // persediaan + 'debit'=>$nilai, + 'kredit'=>0 + ], + [ + 'journal_id'=>$jid, + 'account_id'=>65, // modal / selisih + 'debit'=>0, + 'kredit'=>$nilai + ] + ]); + }else{ + // stok turun + $this->db->insert_batch('journal_details',[ + [ + 'journal_id'=>$jid, + 'account_id'=>64, + 'debit'=>$nilai, + 'kredit'=>0 + ], + [ + 'journal_id'=>$jid, + 'account_id'=>21, + 'debit'=>0, + 'kredit'=>$nilai + ] + ]); + } + + $this->db->trans_complete(); + + log_activity( + 'items', + 'adjust', + 'Adjustment item ID: ' . $item_id . ' qty: ' . $qty, + 'success' + ); + + $this->update_item_stok($item_id); + + echo json_encode(['status'=>true,'message'=>'Adjustment berhasil']); + } + + private function update_item_stok($item_id) + { + $stok = $this->db->query(" + SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok + FROM stock_logs + WHERE item_id = ? + ", [$item_id])->row()->stok; + + $this->db->where('id', $item_id) + ->update('items', [ + 'stok' => $stok + ]); + } + + public function get_items_by_wh_id($warehouse_id) + { + $this->db->select(' + items.id, + items.nama_barang, + items.harga_jual, + items.kode_detail, + COALESCE(SUM( + stock_logs.qty * IF(stock_logs.tipe="masuk",1,-1) + ),0) as stok + '); + $this->db->where('status', 'active'); + $this->db->from('items'); + + $this->db->join('stock_logs', + 'stock_logs.item_id = items.id + AND stock_logs.warehouse_id = '.$this->db->escape($warehouse_id), + 'left'); + + $this->db->group_by('items.id'); + + // ✅ INI KUNCINYA + $this->db->having('stok >', 0); + + + $data = $this->db->get()->result(); + + echo json_encode($data); + } +} \ No newline at end of file diff --git a/application/controllers/Jurnal.php b/application/controllers/Jurnal.php new file mode 100644 index 0000000..198639d --- /dev/null +++ b/application/controllers/Jurnal.php @@ -0,0 +1,333 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + + // if($this->session->userdata('role') != 'Admin'){ + // show_error('Akses ditolak', 403); + // } + + $data = ["active_menu" => "jurnal"]; + $this->load->view('partials/header', $data); + $this->load->view('jurnal/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET DATA DATATABLE (SUDAH ADA TOTAL) + // ========================= + public function get_data() + { + $draw = $_POST['draw']; + $start = $_POST['start']; + $length = $_POST['length']; + $search = $_POST['search']['value']; + + // ================= TOTAL ALL + $totalAll = $this->db->count_all('journals'); + + // ================= QUERY FILTERED (HITUNG) + $this->db->from('journals'); + + if(!empty($search)){ + $this->db->group_start(); + $this->db->like('no_ref', $search); + $this->db->or_like('keterangan', $search); + $this->db->or_like('tanggal', $search); + $this->db->group_end(); + } + + $filtered = $this->db->count_all_results(); + + // ================= QUERY DATA (AMBIL DATA) + $this->db->select(' + journals.id, + journals.tanggal, + journals.no_ref, + journals.keterangan, + COALESCE(SUM(journal_details.debit),0) as total_debit, + COALESCE(SUM(journal_details.kredit),0) as total_kredit + '); + $this->db->from('journals'); + $this->db->join('journal_details','journal_details.journal_id = journals.id','LEFT'); + + if(!empty($search)){ + $this->db->group_start(); + $this->db->like('journals.no_ref', $search); + $this->db->or_like('journals.keterangan', $search); + $this->db->or_like('journals.tanggal', $search); + $this->db->group_end(); + } + + $this->db->group_by('journals.id'); + $this->db->order_by('journals.tanggal','DESC'); + $this->db->order_by('journals.created_at','DESC'); + $this->db->limit($length, $start); + + $query = $this->db->get()->result(); + + // ================= FORMAT + $data = []; + $no = $start; + + foreach ($query as $row) { + $no++; + + if($this->session->userdata('role') != 'Admin'){ + + $aksi = ' + + '; + + } + else { + $aksi = ' + + + '; + } + + $data[] = [ + $no, + $row->tanggal, + $row->no_ref, + $row->keterangan, + number_format($row->total_debit,0,',','.'), + number_format($row->total_kredit,0,',','.'), + $aksi + ]; + } + + echo json_encode([ + "draw" => $draw, + "recordsTotal" => $totalAll, + "recordsFiltered" => $filtered, + "data" => $data + ]); + } + + // ========================= + // GET ACCOUNTS + // ========================= + public function get_accounts() + { + $data = $this->db->order_by('kode_akun','ASC')->get('accounts')->result(); + echo json_encode($data); + } + + // ========================= + // SAVE + // ========================= + public function save() + { + $tanggal = $this->input->post('tanggal'); + $no_ref = 'JR-' . date('YmdHis'); + $keterangan = $this->input->post('keterangan'); + + $accounts = $this->input->post('account_id'); + $debit = $this->input->post('debit'); + $kredit = $this->input->post('kredit'); + + if (array_sum($debit) != array_sum($kredit)) { + echo json_encode([ + 'status'=>false, + 'message'=>'Debit & Kredit tidak balance!' + ]); + return; + } + + $this->db->trans_start(); + + $this->db->insert('journals', [ + 'tanggal' => $tanggal, + 'no_ref' => $no_ref, + 'keterangan' => $keterangan, + 'created_by' => $this->session->userdata('user_id') + ]); + + $journal_id = $this->db->insert_id(); + + for ($i=0; $idb->insert('journal_details', [ + 'journal_id' => $journal_id, + 'account_id' => $accounts[$i], + 'debit' => $debit[$i] ?: 0, + 'kredit' => $kredit[$i] ?: 0 + ]); + } + + $this->db->trans_complete(); + + if ($this->db->trans_status() === FALSE) { + + echo json_encode([ + 'status'=>false, + 'message'=>'Gagal simpan jurnal' + ]); + return; + + } + + // ================= LOG ACTIVITY ================= + log_activity( + 'Jurnal', + 'create', + 'Membuat jurnal No Ref: '.$no_ref, + 'success' + ); + + echo json_encode([ + 'status'=>true, + 'message'=>'Jurnal berhasil disimpan' + ]); + } + + // ========================= + // DETAIL + // ========================= + public function detail($id) + { + $header = $this->db->get_where('journals',['id'=>$id])->row(); + + $detail = $this->db + ->select('accounts.kode_akun, accounts.nama_akun, journal_details.*') + ->from('journal_details') + ->join('accounts','accounts.id = journal_details.account_id') + ->where('journal_id',$id) + ->get()->result(); + + // print_r($header); + + $userid = $header->created_by; + $user = $this->db + ->select('nama') + ->where('id', $userid) + ->get('users') + ->row(); + + echo json_encode([ + 'header'=>$header, + 'detail'=>$detail, + 'user' =>$user + ]); + } + + // ========================= + // DELETE + // ========================= + public function delete($id) + { + $journal = $this->db->get_where('journals', ['id' => $id])->row(); + + // VALIDASI + if (!$journal) { + echo json_encode([ + 'status' => false, + 'message' => 'Jurnal tidak ditemukan' + ]); + return; + } + + $this->db->trans_start(); + + // ========================= + // HANDLE STOCK LOGS + // ========================= + if ($journal->ref_type == 'stock_logs') { + + $stock = $this->db + ->select('item_id, warehouse_id') + ->where('id', $journal->ref_id) + ->get('stock_logs') + ->row(); + + if ($stock) { + + // hapus log stok + $this->db->delete('stock_logs', ['id' => $journal->ref_id]); + + // update stok ulang (pakai item_id saja biar gak ganggu sistem lama) + $this->update_item_stok($stock->item_id); + } + } + + // ========================= + // DELETE JOURNAL + // ========================= + $this->db->delete('journal_details', ['journal_id' => $id]); + $this->db->delete('journals', ['id' => $id]); + + $this->db->trans_complete(); + + // ========================= + // VALIDASI TRANSAKSI + // ========================= + if (!$this->db->trans_status()) { + + echo json_encode([ + 'status' => false, + 'message' => 'Gagal hapus jurnal' + ]); + return; + } + + // ================= LOG ACTIVITY ================= + log_activity( + 'Jurnal', + 'delete', + 'Menghapus jurnal no_ref: ' . $journal->no_ref . ' ket: ' . $journal->keterangan, + 'success' + ); + + echo json_encode([ + 'status' => true, // ✅ FIX + 'message' => 'Jurnal berhasil dihapus', + 'journal' => $journal->ref_type + ]); + } + + public function get_base_jurnal() + { + echo json_encode( + $this->db->order_by('kode','ASC')->get('base_journal')->result() + ); + } + + public function get_base_jurnal_detail($id) + { + echo json_encode( + $this->db->where('base_journal_id',$id) + ->get('base_journal_detail') + ->result() + ); + } + + private function update_item_stok($item_id) + { + $stok = $this->db->query(" + SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok + FROM stock_logs + WHERE item_id = ? + ", [$item_id])->row()->stok; + + $this->db->where('id', $item_id) + ->update('items', [ + 'stok' => $stok + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Kodebarang.php b/application/controllers/Kodebarang.php new file mode 100644 index 0000000..ca68418 --- /dev/null +++ b/application/controllers/Kodebarang.php @@ -0,0 +1,155 @@ +session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + $data = ["active_menu" => "kode_barang"]; + $this->load->view('partials/header', $data); + $this->load->view('kode_barang/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET DATA (DATATABLE) + // ========================= + public function get_data() + { + $data = $this->db->get('kode_barang')->result(); + + $result = []; + $no = 1; + + foreach ($data as $row) { + $result[] = [ + $no++, + $row->kode_barang, + $row->nama, + $row->limit_stock, + ' + + + ' + ]; + } + + echo json_encode(["data"=>$result]); + } + + // ========================= + // DETAIL + // ========================= + public function detail($id) + { + echo json_encode( + $this->db->get_where('kode_barang',['id'=>$id])->row() + ); + } + + // ========================= + // SAVE + // ========================= + public function save() + { + $nama = $this->input->post('nama'); + $kode = $this->input->post('kode'); + $limit_stock = $this->input->post('limit_stock'); + + if(!$nama){ + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->db->insert('kode_barang', [ + 'kode_barang' => $kode, + 'nama' => $nama, + 'limit_stock' => $limit_stock + ]); + + log_activity( + 'kode_barang', + 'create', + 'Menambahkan kode barang: ' . $nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil ditambahkan']); + } + + // ========================= + // UPDATE + // ========================= + public function update() + { + $id = $this->input->post('id'); + $kode = $this->input->post('kode'); + $nama = $this->input->post('nama'); + $limit_stock = $this->input->post('limit_stock'); + + if(!$nama){ + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->db->where('id',$id)->update('kode_barang', [ + 'kode_barang' => $kode, + 'nama' => $nama, + 'limit_stock' => $limit_stock + ]); + + log_activity( + 'kode_barang', + 'update', + 'Update kode barang ID: ' . $id . ' nama: ' . $nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil diupdate']); + } + + // ========================= + // DELETE + // ========================= + public function delete($id) + { + // optional: cek relasi ke items + $cek = $this->db->get_where('items',['warehouse_id'=>$id])->num_rows(); + + if($cek > 0){ + echo json_encode([ + 'status'=>false, + 'message'=>'Gudang tidak bisa dihapus karena masih dipakai item' + ]); + return; + } + + $this->db->delete('kode_barang',['id'=>$id]); + + log_activity( + 'kode_barang', + 'delete', + 'Hapus gudang ID: ' . $id, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil dihapus']); + } + + + public function list() + { + echo json_encode( + $this->db->get('kode_barang')->result() + ); + } +} \ No newline at end of file diff --git a/application/controllers/Labarugi.php b/application/controllers/Labarugi.php new file mode 100644 index 0000000..6328df7 --- /dev/null +++ b/application/controllers/Labarugi.php @@ -0,0 +1,88 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = ["active_menu" => "laba_rugi"]; + $this->load->view('partials/header', $data); + $this->load->view('laporan/laba_rugi'); + $this->load->view('partials/footer'); + } + + public function get_data() + { + $tanggal_dari = $this->input->get('tanggal_dari'); + $tanggal_sampai = $this->input->get('tanggal_sampai'); + + $this->db->select(' + a.id, + a.kode_akun, + a.nama_akun, + a.tipe, + COALESCE(SUM(jd.debit),0) as debit, + COALESCE(SUM(jd.kredit),0) as kredit + '); + $this->db->from('accounts a'); + + // ================= JOIN + $this->db->join('journal_details jd','a.id = jd.account_id','LEFT'); + $this->db->join('journals j','j.id = jd.journal_id','LEFT'); + + // ================= FILTER AKUN + $this->db->where('a.kategori','laba_rugi'); + $this->db->where('a.is_active',1); + + // ================= FILTER TANGGAL (AMAN) + if($tanggal_dari){ + $this->db->where('(j.tanggal >= "'.$tanggal_dari.'" OR j.tanggal IS NULL)'); + } + + if($tanggal_sampai){ + $this->db->where('(j.tanggal <= "'.$tanggal_sampai.'" OR j.tanggal IS NULL)'); + } + + $this->db->group_by('a.id'); + $this->db->order_by('a.kode_akun','ASC'); + + $rows = $this->db->get()->result(); + + $revenue = 0; + $expense = 0; + + foreach ($rows as $r) { + + if ($r->tipe == 'revenue') { + $saldo = $r->kredit - $r->debit; + $revenue += $saldo; + } + + if ($r->tipe == 'expense') { + $saldo = $r->debit - $r->kredit; + $expense += $saldo; + } + } + + echo json_encode([ + 'data' => $rows, + 'revenue' => $revenue, + 'expense' => $expense, + 'laba' => $revenue - $expense + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Leaverequests.php b/application/controllers/Leaverequests.php new file mode 100644 index 0000000..0a5df73 --- /dev/null +++ b/application/controllers/Leaverequests.php @@ -0,0 +1,284 @@ +load->database(); + } + + // ========================================== + // PAGE + // ========================================== + public function index() + { + $data = [ + "active_menu" => "leave_requests" + ]; + + $data['employees'] = $this->db + ->where('is_active',1) + ->order_by('full_name','ASC') + ->get('k_employees') + ->result(); + + $this->load->view('partials/header', $data); + $this->load->view('employees/leave_requests', $data); + $this->load->view('partials/footer'); + } + + // ========================================== + // DATATABLE + // ========================================== + public function get_data() + { + $this->db->select(' + lr.*, + e.full_name, + e.fingerprint_user_id + '); + + $this->db->from('k_leave_requests lr'); + + $this->db->join( + 'k_employees e', + 'e.id=lr.employee_id', + 'left' + ); + + $this->db->order_by('lr.id','DESC'); + + $list = $this->db->get()->result(); + + $data = []; + $no = 1; + + foreach($list as $r){ + + // ===================================== + // LEAVE TYPE + // ===================================== + $leave_type = '-'; + + if($r->leave_type == 'annual'){ + $leave_type = 'Cuti Tahunan'; + } + + if($r->leave_type == 'sick'){ + $leave_type = 'Sakit'; + } + + if($r->leave_type == 'permit'){ + $leave_type = 'Izin'; + } + + if($r->leave_type == 'maternity'){ + $leave_type = 'Cuti Melahirkan'; + } + + if($r->leave_type == 'unpaid'){ + $leave_type = 'Cuti Tidak Dibayar'; + } + + // ===================================== + // STATUS + // ===================================== + if($r->approval_status == 'approved'){ + + $status = ' + + Disetujui + + '; + + } elseif($r->approval_status == 'rejected'){ + + $status = ' + + Ditolak + + '; + + } else { + + $status = ' + + Pending + + '; + } + + $data[] = [ + $no++, + $r->fingerprint_user_id, + $r->full_name, + $leave_type, + $r->start_date, + $r->end_date, + $r->total_days . ' Hari', + $status, + ' + + + + ' + ]; + } + + echo json_encode([ + "data" => $data + ]); + } + + // ========================================== + // DETAIL + // ========================================== + public function detail($id) + { + $data = $this->db + ->get_where( + 'k_leave_requests', + ['id'=>$id] + ) + ->row(); + + echo json_encode($data); + } + + // ========================================== + // SAVE + // ========================================== + public function save() + { + try{ + + $start_date = $this->input->post('start_date'); + $end_date = $this->input->post('end_date'); + + $total_days = ( + strtotime($end_date) + - + strtotime($start_date) + ) / 86400 + 1; + + $data = [ + + 'employee_id' => $this->input->post('employee_id'), + + 'leave_type' => $this->input->post('leave_type'), + + 'start_date' => $start_date, + + 'end_date' => $end_date, + + 'total_days' => $total_days, + + 'reason' => $this->input->post('reason'), + + 'approval_status' => $this->input->post('approval_status') + ]; + + $this->db->insert( + 'k_leave_requests', + $data + ); + + echo json_encode([ + 'status' => true, + 'message'=> 'Leave request berhasil ditambahkan' + ]); + + } catch(Exception $e){ + + echo json_encode([ + 'status' => false, + 'message'=> $e->getMessage() + ]); + } + } + + // ========================================== + // UPDATE + // ========================================== + public function update() + { + try{ + + $id = $this->input->post('id'); + + $start_date = $this->input->post('start_date'); + $end_date = $this->input->post('end_date'); + + $total_days = ( + strtotime($end_date) + - + strtotime($start_date) + ) / 86400 + 1; + + $data = [ + + 'employee_id' => $this->input->post('employee_id'), + + 'leave_type' => $this->input->post('leave_type'), + + 'start_date' => $start_date, + + 'end_date' => $end_date, + + 'total_days' => $total_days, + + 'reason' => $this->input->post('reason'), + + 'approval_status' => $this->input->post('approval_status') + ]; + + $this->db->where('id',$id); + + $this->db->update( + 'k_leave_requests', + $data + ); + + echo json_encode([ + 'status' => true, + 'message'=> 'Leave request berhasil diupdate' + ]); + + } catch(Exception $e){ + + echo json_encode([ + 'status' => false, + 'message'=> $e->getMessage() + ]); + } + } + + // ========================================== + // DELETE + // ========================================== + public function delete($id) + { + $this->db->delete( + 'k_leave_requests', + ['id'=>$id] + ); + + echo json_encode([ + 'status' => true, + 'message'=> 'Leave request berhasil dihapus' + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Lokasiasset.php b/application/controllers/Lokasiasset.php new file mode 100644 index 0000000..1308531 --- /dev/null +++ b/application/controllers/Lokasiasset.php @@ -0,0 +1,145 @@ +session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + $data = ["active_menu" => "lokasiasset"]; + $this->load->view('partials/header', $data); + $this->load->view('lokasi_asset/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET DATA (DATATABLE) + // ========================= + public function get_data() + { + $data = $this->db->get('lokasi_asset')->result(); + + $result = []; + $no = 1; + + foreach ($data as $row) { + $result[] = [ + $no++, + $row->nama, + ' + + + ' + ]; + } + + echo json_encode(["data"=>$result]); + } + + // ========================= + // DETAIL + // ========================= + public function detail($id) + { + echo json_encode( + $this->db->get_where('lokasi_asset',['id'=>$id])->row() + ); + } + + // ========================= + // SAVE + // ========================= + public function save() + { + $nama = $this->input->post('nama'); + + if(!$nama){ + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->db->insert('lokasi_asset', [ + 'nama' => $nama + ]); + + log_activity( + 'kode_barang', + 'create', + 'Tambah gudang '.$nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil ditambahkan']); + } + + // ========================= + // UPDATE + // ========================= + public function update() + { + $id = $this->input->post('id'); + $nama = $this->input->post('nama'); + + if(!$nama){ + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->db->where('id',$id)->update('lokasi_asset', [ + 'nama' => $nama + ]); + + log_activity( + 'kode_barang', + 'update', + 'Update gudang '.$nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil diupdate']); + } + + // ========================= + // DELETE + // ========================= + public function delete($id) + { + // optional: cek relasi ke items + $cek = $this->db->get_where('assets',['lokasi_asset_id'=>$id])->num_rows(); + + if($cek > 0){ + echo json_encode([ + 'status'=>false, + 'message'=>'Gudang tidak bisa dihapus karena masih dipakai item' + ]); + return; + } + + $this->db->delete('lokasi_asset',['id'=>$id]); + + log_activity( + 'kode_barang', + 'delete', + 'Delete gudang '.$nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil dihapus']); + } + + + public function list() + { + echo json_encode( + $this->db->get('lokasi_asset')->result() + ); + } +} \ No newline at end of file diff --git a/application/controllers/Neraca.php b/application/controllers/Neraca.php new file mode 100644 index 0000000..837f189 --- /dev/null +++ b/application/controllers/Neraca.php @@ -0,0 +1,170 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = ["active_menu" => "neraca"]; + $this->load->view('partials/header', $data); + $this->load->view('laporan/neraca'); + $this->load->view('partials/footer'); + } + + // ========================= + // LABA BERJALAN (PAKAI TANGGAL JOURNAL) + // ========================= + private function get_laba_berjalan($tanggal_dari = null, $tanggal_sampai = null) + { + $this->db->select(' + accounts.tipe, + COALESCE(SUM(jd.debit),0) as debit, + COALESCE(SUM(jd.kredit),0) as kredit + '); + $this->db->from('accounts'); + $this->db->join('journal_details jd','jd.account_id = accounts.id','LEFT'); + $this->db->join('journals j','j.id = jd.journal_id','LEFT'); + + $this->db->where_in('accounts.tipe',['revenue','expense']); + + // ================= FILTER TANGGAL + if($tanggal_dari){ + $this->db->where('(j.tanggal >= "'.$tanggal_dari.'" OR j.tanggal IS NULL)'); + } + + if($tanggal_sampai){ + $this->db->where('(j.tanggal <= "'.$tanggal_sampai.'" OR j.tanggal IS NULL)'); + } + + $rows = $this->db->get()->result(); + + $revenue = 0; + $expense = 0; + + foreach ($rows as $r) { + if ($r->tipe == 'revenue') { + $revenue += ($r->kredit - $r->debit); + } else { + $expense += ($r->debit - $r->kredit); + } + } + + return $revenue - $expense; + } + + // ========================= + // GET DATA NERACA + // ========================= + public function get_data() + { + $tanggal_dari = $this->input->get('tanggal_dari'); + $tanggal_sampai = $this->input->get('tanggal_sampai'); + + $this->db->select(' + a.id, + a.kode_akun, + a.nama_akun, + a.tipe, + a.posisi, + a.is_kontra, + COALESCE(SUM(jd.debit),0) as debit, + COALESCE(SUM(jd.kredit),0) as kredit + '); + $this->db->from('accounts a'); + $this->db->join('journal_details jd','jd.account_id = a.id','LEFT'); + $this->db->join('journals j','j.id = jd.journal_id','LEFT'); + + $this->db->where('a.kategori','neraca'); + + // ================= FILTER TANGGAL + if($tanggal_dari){ + $this->db->where('(j.tanggal >= "'.$tanggal_dari.'" OR j.tanggal IS NULL)'); + } + + if($tanggal_sampai){ + $this->db->where('(j.tanggal <= "'.$tanggal_sampai.'" OR j.tanggal IS NULL)'); + } + + $this->db->group_by('a.id'); + $this->db->order_by('a.kode_akun','ASC'); + + $rows = $this->db->get()->result(); + + $activa = []; + $pasiva = []; + + $total_activa = 0; + $total_pasiva = 0; + + // ================= LABA BERJALAN + $laba = $this->get_laba_berjalan($tanggal_dari, $tanggal_sampai); + + foreach ($rows as $r) { + + // ================= SALDO NORMAL + if ($r->posisi == 'debit') { + $saldo = $r->debit - $r->kredit; + } else { + $saldo = $r->kredit - $r->debit; + } + + // ================= KONTRA ACCOUNT + if ($r->is_kontra == 1) { + $saldo = -$saldo; + } + + // ================= LABA BERJALAN + if ($r->kode_akun == '302') { + $saldo = $laba; + } + + // ================= ACTIVA + if ($r->tipe == 'asset') { + + $activa[] = [ + 'kode' => $r->kode_akun, + 'nama' => $r->nama_akun, + 'saldo' => $saldo + ]; + + $total_activa += $saldo; + } + + // ================= PASIVA + if (in_array($r->tipe, ['liability','equity'])) { + + $pasiva[] = [ + 'kode' => $r->kode_akun, + 'nama' => $r->nama_akun, + 'saldo' => $saldo + ]; + + $total_pasiva += $saldo; + } + } + + echo json_encode([ + 'activa' => $activa, + 'pasiva' => $pasiva, + 'total_activa' => $total_activa, + 'total_pasiva' => $total_pasiva, + 'laba' => $laba, + 'selisih' => $total_activa - $total_pasiva + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Neracasaldo.php b/application/controllers/Neracasaldo.php new file mode 100644 index 0000000..2d6aa43 --- /dev/null +++ b/application/controllers/Neracasaldo.php @@ -0,0 +1,112 @@ +load->model('DynamicModel', 'dm'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = ["active_menu" => "neraca_saldo"]; + $this->load->view('partials/header', $data); + $this->load->view('laporan/neraca_saldo'); + $this->load->view('partials/footer'); + } + + // ========================= + // HITUNG LABA BERJALAN (REALTIME) + // ========================= + private function get_laba_berjalan() + { + $this->db->select(' + accounts.tipe, + COALESCE(SUM(journal_details.debit),0) as debit, + COALESCE(SUM(journal_details.kredit),0) as kredit + '); + $this->db->from('accounts'); + $this->db->join('journal_details','journal_details.account_id = accounts.id','LEFT'); + $this->db->where_in('accounts.tipe',['revenue','expense']); + + $rows = $this->db->get()->result(); + + $revenue = 0; + $expense = 0; + + foreach ($rows as $r) { + if ($r->tipe == 'revenue') { + $revenue += ($r->kredit - $r->debit); + } else { + $expense += ($r->debit - $r->kredit); + } + } + + return $revenue - $expense; + } + + public function get_data() + { + $this->db->select(' + accounts.kode_akun, + accounts.nama_akun, + accounts.tipe, + accounts.posisi, + COALESCE(SUM(journal_details.debit),0) as debit, + COALESCE(SUM(journal_details.kredit),0) as kredit + '); + $this->db->from('accounts'); + $this->db->join('journal_details','journal_details.account_id = accounts.id','LEFT'); + + // OPTIONAL (best practice) + $this->db->where('accounts.is_active',1); + $this->db->where('accounts.kategori','neraca'); + + $this->db->group_by('accounts.id'); + $this->db->order_by('accounts.kode_akun','ASC'); + + $rows = $this->db->get()->result(); + + // ========================= + // HITUNG LABA BERJALAN + // ========================= + $laba = $this->get_laba_berjalan(); + + foreach ($rows as $r) { + + // ================= SALDO NORMAL + if ($r->posisi == 'debit') { + $saldo = $r->debit - $r->kredit; + } else { + $saldo = $r->kredit - $r->debit; + } + + // ================= OVERRIDE LABA BERJALAN + if ($r->kode_akun == '302') { + $saldo = $laba; + } + + // ================= NORMALISASI KE KOLOM + if ($saldo >= 0 AND $r->posisi == 'debit') { + $r->saldo_debit = $saldo; + $r->saldo_kredit = 0; + } else { + $r->saldo_debit = 0; + $r->saldo_kredit = abs($saldo); + } + } + + echo json_encode($rows); + } +} \ No newline at end of file diff --git a/application/controllers/Payroll.php b/application/controllers/Payroll.php new file mode 100644 index 0000000..defe58e --- /dev/null +++ b/application/controllers/Payroll.php @@ -0,0 +1,598 @@ +load->database(); + + date_default_timezone_set('Asia/Jakarta'); + } + + // ===================================================== + // INDEX + // ===================================================== + public function index() + { + $data = [ + 'active_menu' => 'payroll' + ]; + + $this->load->view('partials/header',$data); + $this->load->view('employees/payroll'); + $this->load->view('partials/footer'); + } + + // ===================================================== + // GET DATA + // ===================================================== + public function get_data() + { + $month = $this->input->post('month'); + + $this->db->select(" + p.*, + e.full_name, + pp.period_name, + pp.start_date, + pp.end_date + "); + + $this->db->from('k_payrolls p'); + + $this->db->join( + 'k_employees e', + 'e.id = p.employee_id' + ); + + $this->db->join( + 'k_payroll_periods pp', + 'pp.id = p.payroll_period_id' + ); + + if($month){ + + $this->db->where( + 'DATE_FORMAT(pp.start_date,"%Y-%m")', + $month + ); + } + + $this->db->order_by( + 'e.full_name', + 'ASC' + ); + + $rows = $this->db->get()->result(); + + $data = []; + + $no = 1; + + foreach($rows as $row){ + + $pendapatan = + ( + $row->allowance_amount + + + $row->bonus_amount + + + $row->overtime_amount + ); + + $potongan = + ( + $row->deduction_amount + + + $row->bpjs_kesehatan_amount + + + $row->bpjs_ketenagakerjaan_amount + + + $row->tax_amount + + + $row->attendance_cut_amount + ); + + $status = ''; + + if($row->payment_status == 'paid'){ + + $status = ' + + Paid + + '; + + } else { + + $status = ' + + Draft + + '; + } + + $data[] = [ + + $no++, + + $row->full_name, + + 'Rp '.number_format( + $row->basic_salary, + 0, + ',', + '.' + ), + + 'Rp '.number_format( + $pendapatan, + 0, + ',', + '.' + ), + + 'Rp '.number_format( + $potongan, + 0, + ',', + '.' + ), + + 'Rp '.number_format( + $row->total_salary, + 0, + ',', + '.' + ), + + $status, + + ' + + ' + ]; + } + + echo json_encode([ + 'data' => $data + ]); + } + + // ===================================================== + // DETAIL + // ===================================================== + public function detail($id) + { + $this->db->select(" + p.*, + e.full_name + "); + + $this->db->from('k_payrolls p'); + + $this->db->join( + 'k_employees e', + 'e.id = p.employee_id' + ); + + $this->db->where( + 'p.id', + $id + ); + + $payroll = $this->db->get()->row(); + + if(!$payroll){ + + echo json_encode([ + 'status' => false, + 'message' => 'Payroll tidak ditemukan' + ]); + + return; + } + + $items = $this->db + ->where('payroll_id',$id) + ->order_by('id','ASC') + ->get('k_payroll_items') + ->result(); + + echo json_encode([ + + 'status' => true, + + 'payroll' => $payroll, + + 'employee' => [ + 'full_name' => $payroll->full_name + ], + + 'items' => $items + ]); + } + + // ===================================================== + // SAVE ITEMS + // ===================================================== + public function save_items() + { + $payrollId = $this->input->post('payroll_id'); + + $items = $this->input->post('items'); + + $payroll = $this->db + ->where('id',$payrollId) + ->get('k_payrolls') + ->row(); + + if(!$payroll){ + + echo json_encode([ + 'status' => false, + 'message' => 'Payroll tidak ditemukan' + ]); + + return; + } + + // ========================================= + // DELETE OLD ITEMS + // ========================================= + $this->db + ->where('payroll_id',$payrollId) + ->delete('k_payroll_items'); + + $allowance = 0; + $bonus = 0; + $overtime = 0; + $deduction = 0; + $bpjs = 0; + $tax = 0; + + if($items){ + + foreach($items as $item){ + + $amount = (float)$item['amount']; + + $insert = [ + + 'payroll_id' => $payrollId, + + 'item_type' => $item['item_type'], + + 'item_name' => $item['item_name'], + + 'amount' => $amount + ]; + + $this->db->insert( + 'k_payroll_items', + $insert + ); + + switch($item['item_type']){ + + case 'allowance': + $allowance += $amount; + break; + + case 'bonus': + $bonus += $amount; + break; + + case 'overtime': + $overtime += $amount; + break; + + case 'deduction': + $deduction += $amount; + break; + + case 'bpjs': + $bpjs += $amount; + break; + + case 'tax': + $tax += $amount; + break; + } + } + } + + // ========================================= + // TOTAL + // ========================================= + $totalSalary = + ( + $payroll->basic_salary + + + $allowance + + + $bonus + + + $overtime + ) + - + ( + $deduction + + + $bpjs + + + $tax + + + $payroll->attendance_cut_amount + ); + + // ========================================= + // UPDATE PAYROLL + // ========================================= + $this->db + ->where('id',$payrollId) + ->update( + 'k_payrolls', + [ + + 'allowance_amount' => $allowance, + + 'bonus_amount' => $bonus, + + 'overtime_amount' => $overtime, + + 'deduction_amount' => $deduction, + + 'bpjs_kesehatan_amount' => $bpjs, + + 'tax_amount' => $tax, + + 'total_salary' => $totalSalary + ] + ); + + echo json_encode([ + 'status' => true, + 'message' => 'Payroll berhasil disimpan' + ]); + } + + // ===================================================== + // GENERATE PAYROLL + // ===================================================== + public function generate_payroll() + { + $month = $this->input->post('month'); + + if(!$month){ + + echo json_encode([ + 'status' => false, + 'message' => 'Bulan wajib dipilih' + ]); + + return; + } + + $startDate = date( + 'Y-m-01', + strtotime($month) + ); + + $endDate = date( + 'Y-m-t', + strtotime($month) + ); + + // ========================================= + // PERIOD + // ========================================= + $period = $this->db + ->where('start_date',$startDate) + ->where('end_date',$endDate) + ->get('k_payroll_periods') + ->row(); + + if(!$period){ + + $this->db->insert( + 'k_payroll_periods', + [ + + 'period_name' => date( + 'F Y', + strtotime($startDate) + ), + + 'start_date' => $startDate, + + 'end_date' => $endDate, + + 'status' => 'draft', + + 'generated_at' => date('Y-m-d H:i:s') + ] + ); + + $periodId = $this->db->insert_id(); + + } else { + + $periodId = $period->id; + } + + // ========================================= + // EMPLOYEES + // ========================================= + $employees = $this->db + ->where('is_active',1) + ->get('k_employees') + ->result(); + + foreach($employees as $employee){ + + // ===================================== + // ATTENDANCE + // ===================================== + $attendance = $this->db + ->select(" + COUNT(*) as total_work_days, + + SUM(attendance_status='present') + as total_present, + + SUM(attendance_status='alpha') + as total_alpha, + + SUM(overtime_hours) + as total_overtime_hours + ") + ->where('employee_id',$employee->id) + ->where('attendance_date >=',$startDate) + ->where('attendance_date <=',$endDate) + ->get('k_attendances') + ->row(); + + $dailySalary = + $employee->basic_salary / 30; + + $alphaCut = + ( + $attendance->total_alpha + * $dailySalary + ); + + $overtimeRate = 25000; + + $overtimeAmount = + ( + $attendance->total_overtime_hours + * $overtimeRate + ); + + $totalSalary = + ( + $employee->basic_salary + + $overtimeAmount + ) + - $alphaCut; + + $exist = $this->db + ->where( + 'payroll_period_id', + $periodId + ) + ->where( + 'employee_id', + $employee->id + ) + ->get('k_payrolls') + ->row(); + + $data = [ + + 'payroll_period_id' => $periodId, + + 'employee_id' => $employee->id, + + 'basic_salary' => $employee->basic_salary, + + 'total_work_days' => ( + $attendance->total_work_days ?? 0 + ), + + 'total_present' => ( + $attendance->total_present ?? 0 + ), + + 'total_alpha' => ( + $attendance->total_alpha ?? 0 + ), + + 'total_overtime_hours' => ( + $attendance->total_overtime_hours ?? 0 + ), + + 'attendance_cut_amount' => $alphaCut, + + 'overtime_amount' => $overtimeAmount, + + 'total_salary' => $totalSalary + ]; + + if($exist){ + + $this->db + ->where('id',$exist->id) + ->update( + 'k_payrolls', + $data + ); + + } else { + + $this->db->insert( + 'k_payrolls', + $data + ); + + $payrollId = $this->db->insert_id(); + + // ================================= + // AUTO ITEM OVERTIME + // ================================= + if($overtimeAmount > 0){ + + $this->db->insert( + 'k_payroll_items', + [ + + 'payroll_id' => $payrollId, + + 'item_type' => 'overtime', + + 'item_name' => 'Lembur', + + 'amount' => $overtimeAmount + ] + ); + } + + // ================================= + // AUTO ITEM ALPHA + // ================================= + if($alphaCut > 0){ + + $this->db->insert( + 'k_payroll_items', + [ + + 'payroll_id' => $payrollId, + + 'item_type' => 'deduction', + + 'item_name' => 'Potongan Alpha', + + 'amount' => $alphaCut + ] + ); + } + } + } + + echo json_encode([ + 'status' => true, + 'message' => 'Payroll berhasil digenerate' + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Pesan.php b/application/controllers/Pesan.php new file mode 100644 index 0000000..1339d4f --- /dev/null +++ b/application/controllers/Pesan.php @@ -0,0 +1,142 @@ +load->database(); + + // Cek apakah user sudah login + if (!$this->session->userdata('logged_in')) { + redirect('login'); + } + } + + public function index() + { + if (!check_permission('pesan', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $data = [ + "active_menu" => "pesan" + ]; + + $this->load->view('partials/header', $data); + $this->load->view('pesan/layout'); + $this->load->view('partials/footer'); + } + + // ========================================================= + // DATATABLE SERVERSIDE + // ========================================================= + public function get_data() + { + + if (!check_permission('pesan', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $table = 'notifikasi'; + $column_order = array(null, 'created_at', 'type', 'nomor_whatsapp', 'email', 'pesan', 'status'); + $column_search = array('pesan', 'nomor_whatsapp', 'email', 'status', 'type'); + $order = array('created_at' => 'DESC'); + + $this->db->from($table); + + $search_value = !empty($_POST['search']['value']) ? $_POST['search']['value'] : ''; + + // SEARCH + if ($search_value != '') { + $this->db->group_start(); + foreach ($column_search as $i => $col_name) { + if ($i === 0) { + $this->db->like($col_name, $search_value); + } else { + $this->db->or_like($col_name, $search_value); + } + } + $this->db->group_end(); + } + + // ORDER + if (isset($_POST['order'])) { + $col_index = $_POST['order'][0]['column']; + $dir = $_POST['order'][0]['dir']; + if (isset($column_order[$col_index])) { + $this->db->order_by($column_order[$col_index], $dir); + } + } else { + $this->db->order_by(key($order), $order[key($order)]); + } + + // PAGINATION + if ($_POST['length'] != -1) { + $this->db->limit($_POST['length'], $_POST['start']); + } + + $query = $this->db->get(); + $data = []; + $no = $_POST['start']; + + foreach ($query->result() as $row) { + $no++; + + // Badge for 'status' + $statusBadge = ''; + switch ($row->status) { + case 'terkirim': $statusBadge = 'Terkirim'; break; + case 'proses': $statusBadge = 'Proses'; break; + case 'pending': $statusBadge = 'Pending'; break; + case 'dibaca': $statusBadge = 'Dibaca'; break; + default: $statusBadge = '' . htmlspecialchars($row->status) . ''; break; + } + + // Type display + $typeDisplay = ''; + switch ($row->type) { + case 'wa_official': $typeDisplay = 'WhatsApp Official'; break; + case 'wa_unofficial': $typeDisplay = 'WhatsApp Unofficial'; break; + case 'email': $typeDisplay = 'Email'; break; + default: $typeDisplay = htmlspecialchars($row->type); break; + } + + $penerima = !empty($row->nomor_whatsapp) ? htmlspecialchars($row->nomor_whatsapp) : htmlspecialchars($row->email); + + $data[] = [ + $no, + date('d-m-Y H:i:s', strtotime($row->created_at)), + $typeDisplay, + $penerima, + '
' . htmlspecialchars($row->pesan) . '
', + $statusBadge + ]; + } + + // COUNT TOTAL RECORDS + $this->db->from($table); + if ($search_value != '') { + $this->db->group_start(); + foreach ($column_search as $i => $col_name) { + if ($i === 0) { + $this->db->like($col_name, $search_value); + } else { + $this->db->or_like($col_name, $search_value); + } + } + $this->db->group_end(); + } + $recordsFiltered = $this->db->count_all_results(); + + $output = [ + "draw" => intval($_POST['draw']), + "recordsTotal" => $this->db->count_all($table), + "recordsFiltered" => $recordsFiltered, + "data" => $data + ]; + + echo json_encode($output); + } +} diff --git a/application/controllers/Qr.php b/application/controllers/Qr.php new file mode 100644 index 0000000..b82fe6b --- /dev/null +++ b/application/controllers/Qr.php @@ -0,0 +1,50 @@ +session->userdata('logged_in')) { + redirect('login'); + } + } + + // Tampilkan QR (dengan margin kecil) + public function show($text = "contoh") + { + header("Content-Type: image/png"); + + // Param: text, outfile, ECC, size, margin + QRcode::png($text, null, QR_ECLEVEL_H, 8, 1); + } + + // Download QR Code + public function download($text = "contoh") + { + $fileName = "QR_" . time() . ".png"; + $tempDir = FCPATH . "downloads/"; + + if (!is_dir($tempDir)) { + mkdir($tempDir, 0777, true); + } + + $filePath = $tempDir . $fileName; + + // Generate QR ke file (margin diperkecil) + QRcode::png($text, $filePath, QR_ECLEVEL_H, 8, 1); + + // Force download + header('Content-Type: image/png'); + header('Content-Disposition: attachment; filename="'.$fileName.'"'); + readfile($filePath); + + // Hapus setelah download + unlink($filePath); + } +} diff --git a/application/controllers/Scheduler.php b/application/controllers/Scheduler.php new file mode 100644 index 0000000..be0d549 --- /dev/null +++ b/application/controllers/Scheduler.php @@ -0,0 +1,644 @@ +load->database(); + + date_default_timezone_set('Asia/Jakarta'); + } + + // ===================================================== + // PROCESS ATTENDANCE LOG + // ===================================================== + public function process_attendance() + { + echo "PROCESS ATTENDANCE START\n"; + + // ============================================= + // GET PENDING LOG + // ============================================= + $logs = $this->db + ->where('process_status','pending') + ->order_by('check_time','ASC') + ->limit(1000) + ->get('k_attendance_logs') + ->result(); + + foreach($logs as $log){ + + try{ + + // ===================================== + // GET EMPLOYEE + // ===================================== + $employee = $this->db + ->where( + 'fingerprint_user_id', + $log->user_id + ) + ->where('is_active',1) + ->get('k_employees') + ->row(); + + if(!$employee){ + + $this->updateLogStatus( + $log->id, + 'failed' + ); + + echo "FAILED EMPLOYEE : {$log->user_id}\n"; + + continue; + } + + // ===================================== + // UPDATE EMPLOYEE ID + // ===================================== + $this->db + ->where('id',$log->id) + ->update( + 'k_attendance_logs', + [ + 'employee_id' => $employee->id + ] + ); + + $logDateTime = date( + 'Y-m-d H:i:s', + strtotime($log->check_time) + ); + + // ===================================== + // FIND SCHEDULE + // SUPPORT SHIFT MALAM + // ===================================== + $this->db->from( + 'k_employee_shift_schedules' + ); + + $this->db->where( + 'employee_id', + $employee->id + ); + + $this->db->where( + 'status', + 'published' + ); + + // ===================================== + // RANGE SHIFT + // ===================================== + $this->db->where(" + '{$logDateTime}' BETWEEN + DATE_SUB(start_datetime, INTERVAL 3 HOUR) + AND + DATE_ADD(end_datetime, INTERVAL 6 HOUR) + ", null, false); + + $this->db->order_by( + 'start_datetime', + 'DESC' + ); + + $schedule = $this->db->get()->row(); + + // ===================================== + // SCHEDULE NOT FOUND + // ===================================== + if(!$schedule){ + + $this->updateLogStatus( + $log->id, + 'failed' + ); + + echo "FAILED SCHEDULE : {$logDateTime}\n"; + + continue; + } + + // ===================================== + // GET SHIFT + // ===================================== + $shift = $this->db + ->where('id',$schedule->shift_id) + ->get('k_shifts') + ->row(); + + if(!$shift){ + + $this->updateLogStatus( + $log->id, + 'failed' + ); + + echo "FAILED SHIFT : {$schedule->shift_id}\n"; + + continue; + } + + // ===================================== + // ATTENDANCE DATE + // ===================================== + $attendance_date = date( + 'Y-m-d', + strtotime($schedule->start_datetime) + ); + + // ===================================== + // GET ATTENDANCE + // ===================================== + $attendance = $this->db + ->where( + 'employee_id', + $employee->id + ) + ->where( + 'attendance_date', + $attendance_date + ) + ->get('k_attendances') + ->row(); + + // ===================================== + // CHECK LATE + // ===================================== + $late_minutes = 0; + + $maxCheckin = strtotime( + $schedule->start_datetime + ) + ( + $shift->late_tolerance_minutes * 60 + ); + + if( + strtotime($logDateTime) + > + $maxCheckin + ){ + + $late_minutes = floor( + ( + strtotime($logDateTime) + - + strtotime($schedule->start_datetime) + ) / 60 + ); + + if($late_minutes < 0){ + $late_minutes = 0; + } + } + + // ===================================== + // INSERT CHECKIN + // ===================================== + if(!$attendance){ + + $this->db->insert( + 'k_attendances', + [ + + 'employee_id' => $employee->id, + + 'attendance_date' => $attendance_date, + + 'checkin_time' => $logDateTime, + + 'late_minutes' => $late_minutes, + + 'attendance_status' => ( + $late_minutes > 0 + ? 'late' + : 'present' + ), + + 'shift_id' => $shift->id, + + 'attendance_log_in_id' => $log->id, + + 'created_at' => date('Y-m-d H:i:s') + ] + ); + + echo "CHECKIN : {$employee->full_name}\n"; + + } else { + + // ================================= + // SUDAH ADA CHECKOUT + // ================================= + if(!empty($attendance->checkout_time)){ + + $this->updateLogStatus( + $log->id, + 'processed' + ); + + echo "SKIP LOG (SUDAH CHECKOUT)\n"; + + continue; + } + + // ================================= + // CEK SELISIH JAM + // ================================= + $diffMinutes = floor( + ( + strtotime($logDateTime) + - + strtotime($attendance->checkin_time) + ) / 60 + ); + + // ================================= + // MINIMAL 2 JAM + // BARU DIANGGAP CHECKOUT + // ================================= + if($diffMinutes < 120){ + + $this->updateLogStatus( + $log->id, + 'processed' + ); + + echo "SKIP DUPLICATE LOG : {$employee->full_name}\n"; + + continue; + } + + // ================================= + // WORK HOURS + // ================================= + $workHours = ( + strtotime($logDateTime) + - + strtotime($attendance->checkin_time) + ) / 3600; + + if($workHours < 0){ + $workHours = 0; + } + + // ================================= + // EARLY LEAVE + // ================================= + $early_leave_minutes = 0; + + if( + strtotime($logDateTime) + < + strtotime($schedule->end_datetime) + ){ + + $early_leave_minutes = floor( + ( + strtotime($schedule->end_datetime) + - + strtotime($logDateTime) + ) / 60 + ); + + if($early_leave_minutes < 0){ + $early_leave_minutes = 0; + } + } + + // ================================= + // OVERTIME + // ================================= + $overtime_hours = 0; + + if( + strtotime($logDateTime) + > + strtotime($schedule->end_datetime) + ){ + + $overtime_hours = round( + ( + strtotime($logDateTime) + - + strtotime($schedule->end_datetime) + ) / 3600, + 2 + ); + + if($overtime_hours < 0){ + $overtime_hours = 0; + } + } + + // ================================= + // UPDATE CHECKOUT + // ================================= + $this->db + ->where( + 'id', + $attendance->id + ) + ->update( + 'k_attendances', + [ + + 'checkout_time' => $logDateTime, + + 'attendance_log_out_id' => $log->id, + + 'work_hours' => round($workHours,2), + + 'early_leave_minutes' => $early_leave_minutes, + + 'overtime_hours' => $overtime_hours + ] + ); + + echo "CHECKOUT : {$employee->full_name}\n"; + } + + // ===================================== + // UPDATE LOG STATUS + // ===================================== + $this->updateLogStatus( + $log->id, + 'processed' + ); + + echo "SUCCESS LOG ID : {$log->id}\n"; + + } catch(Exception $e){ + + $this->updateLogStatus( + $log->id, + 'failed' + ); + + echo "ERROR : " . $e->getMessage() . "\n"; + } + } + + echo "PROCESS ATTENDANCE DONE\n"; + } + + // ===================================================== + // AUTO CLOSE ATTENDANCE + // ===================================================== + public function auto_close_attendance() + { + echo "AUTO CLOSE START\n"; + + // ============================================= + // AMBIL ATTENDANCE BELUM CHECKOUT + // ============================================= + $attendances = $this->db + ->where('checkout_time IS NULL', null, false) + ->get('k_attendances') + ->result(); + + foreach($attendances as $attendance){ + + // ========================================= + // GET SCHEDULE + // ========================================= + $schedule = $this->db + ->where( + 'employee_id', + $attendance->employee_id + ) + ->where( + 'shift_id', + $attendance->shift_id + ) + ->where( + 'DATE(start_datetime)', + $attendance->attendance_date + ) + ->get('k_employee_shift_schedules') + ->row(); + + if(!$schedule){ + continue; + } + + // ========================================= + // AUTO CLOSE + // END SHIFT + 4 JAM + // ========================================= + $autoCloseTime = date( + 'Y-m-d H:i:s', + strtotime( + $schedule->end_datetime . ' +4 hours' + ) + ); + + // ========================================= + // BELUM WAKTUNYA + // ========================================= + if( + strtotime(date('Y-m-d H:i:s')) + < + strtotime($autoCloseTime) + ){ + continue; + } + + // ========================================= + // WORK HOURS + // ========================================= + $workHours = ( + strtotime($schedule->end_datetime) + - + strtotime($attendance->checkin_time) + ) / 3600; + + if($workHours < 0){ + $workHours = 0; + } + + // ========================================= + // AUTO CHECKOUT + // ========================================= + $this->db + ->where('id',$attendance->id) + ->update( + 'k_attendances', + [ + + 'checkout_time' => $schedule->end_datetime, + + 'work_hours' => round($workHours,2), + + 'notes' => 'Auto checkout by system' + ] + ); + + echo "AUTO CLOSE EMPLOYEE : {$attendance->employee_id}\n"; + } + + echo "AUTO CLOSE DONE\n"; + } + + // ===================================================== + // GENERATE DAILY ATTENDANCE + // ===================================================== + public function generate_daily_attendance() + { + echo "GENERATE DAILY START\n"; + + // ============================================= + // GENERATE HARI KEMARIN + // ============================================= + $date = date( + 'Y-m-d', + strtotime('-1 day') + ); + + // ============================================= + // GET ALL SCHEDULE + // ============================================= + $schedules = $this->db + ->where('schedule_date', $date) + ->where('status','published') + ->get('k_employee_shift_schedules') + ->result(); + + foreach($schedules as $schedule){ + + // ========================================= + // CHECK ATTENDANCE EXIST + // ========================================= + $attendance = $this->db + ->where( + 'employee_id', + $schedule->employee_id + ) + ->where( + 'attendance_date', + $date + ) + ->get('k_attendances') + ->row(); + + if($attendance){ + continue; + } + + // ========================================= + // CHECK HOLIDAY + // ========================================= + $holiday = $this->db + ->where('holiday_date',$date) + ->get('k_holidays') + ->row(); + + if($holiday){ + + $this->db->insert( + 'k_attendances', + [ + + 'employee_id' => $schedule->employee_id, + + 'attendance_date' => $date, + + 'attendance_status' => 'holiday', + + 'is_holiday' => 1, + + 'shift_id' => $schedule->shift_id, + + 'notes' => $holiday->holiday_name + ] + ); + + continue; + } + + // ========================================= + // CHECK LEAVE + // ========================================= + $leave = $this->db + ->where( + 'employee_id', + $schedule->employee_id + ) + ->where( + 'approval_status', + 'approved' + ) + ->where('start_date <=',$date) + ->where('end_date >=',$date) + ->get('k_leave_requests') + ->row(); + + if($leave){ + + $this->db->insert( + 'k_attendances', + [ + + 'employee_id' => $schedule->employee_id, + + 'attendance_date' => $date, + + 'attendance_status' => $leave->leave_type, + + 'shift_id' => $schedule->shift_id, + + 'notes' => $leave->reason + ] + ); + + continue; + } + + // ========================================= + // DEFAULT ALPHA + // ========================================= + $this->db->insert( + 'k_attendances', + [ + + 'employee_id' => $schedule->employee_id, + + 'attendance_date' => $date, + + 'attendance_status' => 'alpha', + + 'shift_id' => $schedule->shift_id, + + 'notes' => 'Tidak hadir' + ] + ); + + echo "GENERATE EMPLOYEE : {$schedule->employee_id}\n"; + } + + echo "GENERATE DAILY DONE\n"; + } + + // ===================================================== + // UPDATE LOG STATUS + // ===================================================== + private function updateLogStatus( + $id, + $status + ){ + + $this->db + ->where('id',$id) + ->update( + 'k_attendance_logs', + [ + 'process_status' => $status + ] + ); + } +} \ No newline at end of file diff --git a/application/controllers/Setting.php b/application/controllers/Setting.php new file mode 100644 index 0000000..cae5cb8 --- /dev/null +++ b/application/controllers/Setting.php @@ -0,0 +1,336 @@ +session->userdata('logged_in')) { + redirect('login'); + } + + if (!check_permission('setting', 'can_view')) { + show_error('Anda tidak memiliki izin untuk mengakses halaman ini.', 403); + } + } + + // ========================== + // MAIN PAGE + // ========================== + public function index() + { + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + // Ambil config id=1 + $cfg = $this->db->get_where("config", ["id" => 1])->row(); + + // Jika tidak ada data config + $config = $cfg ?: $this->defaultConfig(); + + // Ambil template pesan + $template_pesan = $this->db + ->order_by("id", "ASC") + ->get("template_pesan") + ->result(); + + $data = [ + "active_menu" => "pengaturan", + "config" => $config, + "template_pesan" => $template_pesan + ]; + + $this->load->view('partials/header', $data); + $this->load->view('setting/layout', $data); + $this->load->view('partials/footer'); + } + + // =============================== + // DEFAULT CONFIG + // =============================== + private function defaultConfig() + { + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + return (object)[ + "nama_lembaga" => "", + "pimpinan_lembaga" => "", + "card_background" => "", + "card_orientation" => "landscape", + "wa_mode" => "official", + "wa_phone" => "", + "wa_phone_id" => "", + "wa_waba_id" => "", + "wa_access_token" => "", + "wa_endpoint" => "", + "wa_api_key" => "" + ]; + } + + // =============================== + // 1. SIMPAN SETTING UMUM + // =============================== + public function save_umum() + { + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $update = [ + "nama_lembaga" => $this->input->post("nama_lembaga"), + "pimpinan_lembaga" => $this->input->post("pimpinan_lembaga"), + "updated_at" => date("Y-m-d H:i:s") + ]; + + $this->db->where("id", 1)->update("config", $update); + + redirect("setting"); + } + + // =============================== + // 2. SIMPAN CARD + UPLOAD BG + // =============================== + public function save_card() + { + + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $orientation = $this->input->post("card_orientation"); + $bg = null; + + // Jika ada upload file + if (!empty($_FILES["card_background"]["name"])) { + + $config['upload_path'] = './uploads/card/'; + $config['allowed_types'] = 'jpg|jpeg|png'; + $config['file_name'] = 'background_' . time(); + + if (!is_dir('./uploads/card/')) { + mkdir('./uploads/card/', 0777, true); + } + + $this->load->library('upload', $config); + + if ($this->upload->do_upload('card_background')) { + $file = $this->upload->data(); + $bg = "uploads/card/" . $file['file_name']; + } + } + + // Data update + $data = [ + "card_orientation" => $orientation, + "updated_at" => date("Y-m-d H:i:s") + ]; + + // Jika ada gambar baru + if ($bg !== null) { + $data["card_background"] = $bg; + } + + $this->db->where("id", 1)->update("config", $data); + + redirect("setting"); + } + + // =============================== + // 3. SIMPAN SETTING WHATSAPP + // =============================== + public function save_whatsapp() + { + + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $mode = $this->input->post("wa_mode"); + + $data = [ + "wa_mode" => $mode, + "updated_at" => date("Y-m-d H:i:s") + ]; + + if ($mode == "official") { + + $data["wa_phone"] = $this->input->post("wa_phone"); + $data["wa_phone_id"] = $this->input->post("wa_phone_id"); + $data["wa_waba_id"] = $this->input->post("wa_waba_id"); + $data["wa_access_token"] = $this->input->post("wa_access_token"); + + // Kosongkan unofficial + $data["wa_endpoint"] = ""; + $data["wa_api_key"] = ""; + + } else { + + $data["wa_endpoint"] = $this->input->post("wa_endpoint"); + $data["wa_api_key"] = $this->input->post("wa_api_key"); + + // Kosongkan official + $data["wa_phone"] = ""; + $data["wa_phone_id"] = ""; + $data["wa_waba_id"] = ""; + $data["wa_access_token"] = ""; + } + + $this->db->where("id", 1)->update("config", $data); + + redirect("setting"); + } + + // =============================== + // 4. UPDATE TEMPLATE PESAN + // =============================== + public function update_template_pesan() + { + + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $data = [ + "nama_template" => $this->input->post("nama_template"), + "isi_template" => $this->input->post("isi_template") + ]; + + $id = $this->input->post("id"); + + $this->db->where("id", $id)->update("template_pesan", $data); + + redirect("setting"); + } + + public function cek_whatsapp_status() + { + + if (!check_permission('setting', 'can_view')) { + show_error('Tidak memiliki izin untuk melihat detail.', 403); + } + + $config = $this->db->get_where("config", ["id" => 1])->row(); + + if (!$config) { + echo json_encode([ + "status" => "error", + "img" => base_url("uploads/whatsapp/disconnect.png"), + "msg" => "Config tidak ditemukan" + ]); + return; + } + + // Fungsi bantu call API + function callCurl($url, $fields) + { + $curl = curl_init(); + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 0, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_POSTFIELDS => $fields, + CURLOPT_HTTPHEADER => array( 'Content-Type: application/x-www-form-urlencoded' ), + ]); + $response = curl_exec($curl); + curl_close($curl); + return $response; + } + + // 1. CEK STATUS + $response = callCurl( + $config->wa_endpoint . "getState", + "apiKey=" . $config->wa_api_key + ); + + $data = json_decode($response, true); + + if (!$data || !isset($data['results']['state'])) { + echo json_encode([ + "status" => "error", + "img" => base_url('uploads/whatsapp/disconnect.png'), + "msg" => "Gagal membaca respon server WA", + "data" => $data, + "url_key" => $config->wa_endpoint . ' ' . $config->wa_api_key + ]); + return; + } + + $state = $data['results']['state']; + + // ========================================================== + // 1. CONNECTED → tampilkan gambar connected + // ========================================================== + if ($state == "CONNECTED") { + echo json_encode([ + "status" => "CONNECTED", + "img" => base_url('uploads/whatsapp/connect.png') + ]); + return; + } + + // ========================================================== + // 2. SERVICE_OFF → tampilkan disconnect + mulai service + // ========================================================== + if ($state == "SERVICE_OFF") { + + // Mulai service + callCurl( + $config->wa_endpoint . "serviceStart", + "apiKey=" . $config->wa_api_key + ); + + echo json_encode([ + "status" => "SERVICE_OFF", + "img" => base_url('uploads/whatsapp/disconnect.png') + ]); + return; + } + + // ========================================================== + // 3. SERVICE_SCAN → ambil QR + // ========================================================== + if ($state == "SERVICE_SCAN") { + + $responseQR = callCurl( + $config->wa_endpoint . "getQR", + "apiKey=" . $config->wa_api_key + ); + + $qrData = json_decode($responseQR, true); + + if (!isset($qrData['results']['qrString'])) { + echo json_encode([ + "status" => "SERVICE_SCAN", + "img" => base_url('uploads/whatsapp/map_loader.gif') + ]); + return; + } + + echo json_encode([ + "status" => "SERVICE_SCAN", + "img" => $qrData['results']['qrString'] + ]); + return; + } + + // ========================================================== + // 4. STATUS LAIN → tampilkan loader + // ========================================================== + echo json_encode([ + "status" => "UNKNOWN", + "img" => base_url('uploads/whatsapp/map_loader.gif') + ]); + } + +} diff --git a/application/controllers/Shifts.php b/application/controllers/Shifts.php new file mode 100644 index 0000000..f0c2b59 --- /dev/null +++ b/application/controllers/Shifts.php @@ -0,0 +1,147 @@ +load->database(); + } + + // ========================================== + // PAGE + // ========================================== + public function index() + { + $data = [ + "active_menu" => "shift" + ]; + + $this->load->view('partials/header', $data); + $this->load->view('employees/shift'); + $this->load->view('partials/footer'); + } + + // ===================================================== + // DATATABLE + // ===================================================== + public function get_data() + { + $list = $this->db + ->order_by('id','DESC') + ->get('k_shifts') + ->result(); + + $data = []; + $no = 1; + + foreach($list as $r){ + + $night = $r->is_night_shift == 1 + ? 'Night Shift' + : 'Normal'; + + $data[] = [ + $no++, + $r->shift_code, + $r->shift_name, + $r->checkin_time, + $r->checkout_time, + $r->late_tolerance_minutes . ' Menit', + $r->work_hours . ' Jam', + $night, + ' + + + + ' + ]; + } + + echo json_encode([ + "data" => $data + ]); + } + + // ===================================================== + // DETAIL + // ===================================================== + public function detail($id) + { + $data = $this->db + ->get_where('k_shifts',['id'=>$id]) + ->row(); + + echo json_encode($data); + } + + // ===================================================== + // SAVE + // ===================================================== + public function save() + { + $data = [ + 'shift_code' => $this->input->post('shift_code'), + 'shift_name' => $this->input->post('shift_name'), + 'checkin_time' => $this->input->post('checkin_time'), + 'checkout_time' => $this->input->post('checkout_time'), + 'late_tolerance_minutes' => $this->input->post('late_tolerance_minutes'), + 'work_hours' => $this->input->post('work_hours'), + 'is_night_shift' => $this->input->post('is_night_shift') + ]; + + $this->db->insert('k_shifts',$data); + + echo json_encode([ + 'status' => true, + 'message'=> 'Shift berhasil ditambahkan' + ]); + } + + // ===================================================== + // UPDATE + // ===================================================== + public function update() + { + $id = $this->input->post('id'); + + $data = [ + 'shift_code' => $this->input->post('shift_code'), + 'shift_name' => $this->input->post('shift_name'), + 'checkin_time' => $this->input->post('checkin_time'), + 'checkout_time' => $this->input->post('checkout_time'), + 'late_tolerance_minutes' => $this->input->post('late_tolerance_minutes'), + 'work_hours' => $this->input->post('work_hours'), + 'is_night_shift' => $this->input->post('is_night_shift') + ]; + + $this->db->where('id',$id); + $this->db->update('k_shifts',$data); + + echo json_encode([ + 'status' => true, + 'message'=> 'Shift berhasil diupdate' + ]); + } + + // ===================================================== + // DELETE + // ===================================================== + public function delete($id) + { + $this->db->delete('k_shifts',['id'=>$id]); + + echo json_encode([ + 'status' => true, + 'message'=> 'Shift berhasil dihapus' + ]); + } +} \ No newline at end of file diff --git a/application/controllers/Users.php b/application/controllers/Users.php new file mode 100644 index 0000000..aab5763 --- /dev/null +++ b/application/controllers/Users.php @@ -0,0 +1,263 @@ +load->model('DynamicModel', 'dm'); + $this->load->library('form_validation'); + + if (!$this->session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + + if($this->session->userdata('role') != 'Admin'){ + show_error('Akses ditolak', 403); + } + + $data = ["active_menu" => "users"]; + + $this->load->view('partials/header', $data); + $this->load->view('users/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // DATATABLE USERS + ROLES + // ========================= + public function get_data() + { + $this->db->select(' + users.id, + users.username, + users.nama, + roles.nama_role + '); + $this->db->from('users'); + $this->db->join('roles', 'roles.id = users.role_id', 'left'); + + $column_order = ['users.id', 'users.username', 'users.nama', 'roles.nama_role']; + $column_search = ['users.username', 'users.nama', 'roles.nama_role']; + $order = ['users.id' => 'DESC']; + + $search_value = $this->input->post('search')['value'] ?? ''; + + if (!empty($search_value)) { + $this->db->group_start(); + foreach ($column_search as $i => $col) { + if ($i === 0) { + $this->db->like($col, $search_value); + } else { + $this->db->or_like($col, $search_value); + } + } + $this->db->group_end(); + } + + if ($this->input->post('order')) { + $col_index = $this->input->post('order')[0]['column']; + $dir = $this->input->post('order')[0]['dir']; + + if (isset($column_order[$col_index])) { + $this->db->order_by($column_order[$col_index], $dir); + } + } else { + $this->db->order_by(key($order), $order[key($order)]); + } + + if ($this->input->post('length') != -1) { + $this->db->limit( + $this->input->post('length'), + $this->input->post('start') + ); + } + + $query = $this->db->get(); + $data = $query->result(); + + $output = [ + "draw" => intval($this->input->post('draw')), + "recordsTotal" => $this->db->count_all('users'), + "recordsFiltered" => $this->_count_filtered($search_value), + "data" => [] + ]; + + foreach ($data as $row) { + + $action = ' + + + '; + + $output['data'][] = [ + $row->id, + $row->username, + $row->nama, + $row->nama_role, + $action + ]; + } + + echo json_encode($output); + } + + private function _count_filtered($search_value) + { + $this->db->from('users'); + $this->db->join('roles', 'roles.id = users.role_id', 'left'); + + if (!empty($search_value)) { + $this->db->group_start(); + $this->db->like('users.username', $search_value); + $this->db->or_like('users.nama', $search_value); + $this->db->or_like('roles.nama_role', $search_value); + $this->db->group_end(); + } + + return $this->db->count_all_results(); + } + + // ========================= + // STORE USER + // ========================= + public function store() + { + $this->form_validation->set_rules('username', 'Username', 'required|is_unique[users.username]'); + $this->form_validation->set_rules('nama', 'Nama', 'required'); + $this->form_validation->set_rules('role_id', 'Role', 'required'); + $this->form_validation->set_rules('password', 'Password', 'required|min_length[6]'); + + if ($this->form_validation->run() == FALSE) { + echo json_encode([ + 'status' => false, + 'message' => validation_errors() + ]); + return; + } + + $data = [ + 'username' => $this->input->post('username'), + 'nama' => $this->input->post('nama'), + 'role_id' => $this->input->post('role_id'), + 'password' => password_hash($this->input->post('password'), PASSWORD_DEFAULT) + ]; + + $this->dm->setTable('users')->insert($data); + + log_activity( + 'users', + 'create', + 'Menambahkan user: ' . $data['username'], + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'User berhasil ditambahkan' + ]); + } + + // ========================= + // EDIT USER + // ========================= + public function edit($id) + { + $user = $this->db->get_where('users', ['id' => $id])->row(); + + if ($user) { + echo json_encode([ + 'status' => true, + 'data' => $user + ]); + } else { + echo json_encode([ + 'status' => false, + 'message' => 'User tidak ditemukan' + ]); + } + } + + // ========================= + // UPDATE USER + // ========================= + public function update() + { + $id = $this->input->post('id'); + + $this->form_validation->set_rules('username', 'Username', 'required'); + $this->form_validation->set_rules('nama', 'Nama', 'required'); + $this->form_validation->set_rules('role_id', 'Role', 'required'); + + if (!empty($this->input->post('password'))) { + $this->form_validation->set_rules('password', 'Password', 'min_length[6]'); + } + + if ($this->form_validation->run() == FALSE) { + echo json_encode([ + 'status' => false, + 'message' => validation_errors() + ]); + return; + } + + $data = [ + 'username' => $this->input->post('username'), + 'nama' => $this->input->post('nama'), + 'role_id' => $this->input->post('role_id') + ]; + + if (!empty($this->input->post('password'))) { + $data['password'] = password_hash($this->input->post('password'), PASSWORD_DEFAULT); + } + + $this->dm->setTable('users')->update($id, $data); + + log_activity( + 'users', + 'update', + 'Update user ID: ' . $id . ' username: ' . $this->input->post('username'), + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'User berhasil diupdate' + ]); + } + + // ========================= + // DELETE USER + // ========================= + public function delete($id) + { + $this->dm->setTable('users')->delete($id); + + log_activity( + 'users', + 'delete', + 'Hapus user ID: ' . $id, + 'success' + ); + + echo json_encode([ + 'status' => true, + 'message' => 'User berhasil dihapus' + ]); + } + + // ========================= + // GET ROLES + // ========================= + public function get_role() + { + $roles = $this->db->get('roles')->result(); + + echo json_encode($roles); + } +} \ No newline at end of file diff --git a/application/controllers/Warehouses.php b/application/controllers/Warehouses.php new file mode 100644 index 0000000..5c8f9b0 --- /dev/null +++ b/application/controllers/Warehouses.php @@ -0,0 +1,145 @@ +session->userdata('logged_in')) { + redirect('auth'); + } + } + + public function index() + { + $data = ["active_menu" => "warehouses"]; + $this->load->view('partials/header', $data); + $this->load->view('warehouses/layout'); + $this->load->view('partials/footer'); + } + + // ========================= + // GET DATA (DATATABLE) + // ========================= + public function get_data() + { + $data = $this->db->get('warehouses')->result(); + + $result = []; + $no = 1; + + foreach ($data as $row) { + $result[] = [ + $no++, + $row->nama, + ' + + + ' + ]; + } + + echo json_encode(["data"=>$result]); + } + + // ========================= + // DETAIL + // ========================= + public function detail($id) + { + echo json_encode( + $this->db->get_where('warehouses',['id'=>$id])->row() + ); + } + + // ========================= + // SAVE + // ========================= + public function save() + { + $nama = $this->input->post('nama'); + + if(!$nama){ + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->db->insert('warehouses', [ + 'nama' => $nama + ]); + + log_activity( + 'kode_barang', + 'create', + 'Tambah gudang '.$nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil ditambahkan']); + } + + // ========================= + // UPDATE + // ========================= + public function update() + { + $id = $this->input->post('id'); + $nama = $this->input->post('nama'); + + if(!$nama){ + echo json_encode(['status'=>false,'message'=>'Nama wajib diisi']); + return; + } + + $this->db->where('id',$id)->update('warehouses', [ + 'nama' => $nama + ]); + + log_activity( + 'kode_barang', + 'update', + 'Update gudang '.$nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil diupdate']); + } + + // ========================= + // DELETE + // ========================= + public function delete($id) + { + // optional: cek relasi ke items + $cek = $this->db->get_where('items',['warehouse_id'=>$id])->num_rows(); + + if($cek > 0){ + echo json_encode([ + 'status'=>false, + 'message'=>'Gudang tidak bisa dihapus karena masih dipakai item' + ]); + return; + } + + $this->db->delete('warehouses',['id'=>$id]); + + log_activity( + 'kode_barang', + 'delete', + 'Delete gudang '.$nama, + 'success' + ); + + echo json_encode(['status'=>true,'message'=>'Gudang berhasil dihapus']); + } + + + public function list() + { + echo json_encode( + $this->db->get('warehouses')->result() + ); + } +} \ No newline at end of file diff --git a/application/controllers/Welcome.php b/application/controllers/Welcome.php new file mode 100644 index 0000000..e76d042 --- /dev/null +++ b/application/controllers/Welcome.php @@ -0,0 +1,25 @@ + + * @see https://codeigniter.com/userguide3/general/urls.html + */ + public function index() + { + $this->load->view('login'); + } +} diff --git a/application/controllers/index.html b/application/controllers/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/controllers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/core/index.html b/application/core/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/core/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/helpers/activity_helper.php b/application/helpers/activity_helper.php new file mode 100644 index 0000000..d754b15 --- /dev/null +++ b/application/helpers/activity_helper.php @@ -0,0 +1,26 @@ +input->ip_address(); + + if (!$ip || $ip == '0.0.0.0') { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? + $_SERVER['REMOTE_ADDR'] ?? + 'UNKNOWN'; + } + + $CI->db->insert('activity_logs', [ + 'user_id' => $CI->session->userdata('user_id') ?? 0, + 'module' => strtolower($module), + 'action' => strtolower($action), + 'description' => $desc, + 'status' => strtolower($status), + 'ip_address' => $ip, + 'created_at' => date('Y-m-d H:i:s') + ]); +} \ No newline at end of file diff --git a/application/helpers/index.html b/application/helpers/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/helpers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/helpers/indo_date_helper.php b/application/helpers/indo_date_helper.php new file mode 100644 index 0000000..c9da426 --- /dev/null +++ b/application/helpers/indo_date_helper.php @@ -0,0 +1,34 @@ + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember' + ]; + + $timestamp = strtotime($tanggal); + + $hari = date('d', $timestamp); + $bulan_index = (int)date('m', $timestamp); + $tahun = date('Y', $timestamp); + + return $hari . ' ' . $bulan[$bulan_index] . ' ' . $tahun; + } +} \ No newline at end of file diff --git a/application/helpers/permission_helper.php b/application/helpers/permission_helper.php new file mode 100644 index 0000000..ba8a83c --- /dev/null +++ b/application/helpers/permission_helper.php @@ -0,0 +1,30 @@ + nama fitur (misal: 'siswa', 'dashboard') + * @param string $action => action yang dicek (misal: 'can_view', 'can_export') + * @return bool + */ +function check_permission($key, $action = 'can_view') { + $CI =& get_instance(); + $permissions = $CI->session->userdata('permissions'); + + if (!$permissions || !is_array($permissions)) { + return false; + } + + // Pastikan key ada + if (!isset($permissions[$key])) { + return false; + } + + // Jika value berupa array (multiple permissions), cek dengan in_array + if (is_array($permissions[$key])) { + return in_array($action, $permissions[$key]); + } + + // Jika value berupa string tunggal + return $permissions[$key] === $action; +} diff --git a/application/hooks/index.html b/application/hooks/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/hooks/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/index.html b/application/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/english/index.html b/application/language/english/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/language/english/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/index.html b/application/language/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/language/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/libraries/PDF_Invoice.php b/application/libraries/PDF_Invoice.php new file mode 100644 index 0000000..9d4bcc8 --- /dev/null +++ b/application/libraries/PDF_Invoice.php @@ -0,0 +1,255 @@ +nama = $nama; + $this->alamat = $alamat; + $this->telepon = $telepon; + $this->logo = $logo; + } + + function Header() + { + // Logo + $this->Image($this->logo, 10, 8, 22, 22); // Path ke logo Anda, x, y, width + $this->SetXY(35, 11); + $this->SetFont('Arial', 'B', 14); + $this->Cell(0, 6, $this->nama, 0, 1, 'L'); + $this->SetFont('Arial', 'I', 10); + $this->SetXY(35, 17); + $this->Cell(0, 6, $this->alamat, 0, 1, 'L'); + $this->SetXY(35, 23); + $this->Cell(0, 6, 'Phone: '.$this->telepon, 0, 1, 'L'); + $this->Ln(14); + + // Garis Horizontal + $this->SetLineWidth(0.8); // Ketebalan garis + $this->Line(10, 33, 200, 33); // x1, y1, x2, y2 (x1, y1 = start; x2, y2 = end) + $this->SetLineWidth(0.4); + $this->Line(10, 34, 200, 34); + + $this->SetTextColor(255, 140, 0); + $this->SetFont('Arial', 'B', 20); + $this->Cell(0, -56, 'INVOICE', 0, 1, 'R'); + $this->Ln(50); + } + + function Footer() + { + $this->SetY(-15); + $this->SetFont('Arial', 'I', 8); + $this->Cell(0, 10, 'Page ' . $this->PageNo(), 0, 0, 'C'); + } + + function CustomerDetails($customerName, $customerAddress, $invoiceNumber, $invoiceDate, $customerNumber, $invoiceLimit) + { + $this->SetFont('Arial', 'B', 11); + $this->Cell(0, 6, 'Bill To:', 0, 1); + $this->SetFont('Arial', '', 11); + $this->Cell(0, 6, $customerName, 0, 1); + $this->MultiCell(0, 7, $customerAddress); + $this->Cell(0, 6, $customerNumber, 0, 1); + $this->Ln(10); + + $this->SetFont('Arial', '', 11); + $this->SetXY(140, 37); + $this->Cell(0, 6, "No Invoice : $invoiceNumber", 0, 1); + $this->SetXY(140, 44); + $this->Cell(0, 6, "Invoice Date : $invoiceDate", 0, 1); + $this->SetXY(140, 51); + $this->Cell(0, 6, "Limit Invoice : $invoiceLimit", 0, 1); + $this->Ln(10); + } + + function InvoiceTable($header, $data) + { + // 🔥 7 kolom sekarang + $columnWidths = array(8, 22, 30, 70, 15, 20, 25); + $totalWidth = array_sum($columnWidths); + + // scaling biar tetap muat + $scalingFactor = $this->maxWidth / $totalWidth; + + // header style + $this->SetFillColor(255, 140, 0); + $this->SetFont('Arial', '', 10); + + // ================= HEADER ================= + for ($i = 0; $i < count($header); $i++) { + $this->Cell($columnWidths[$i] * $scalingFactor, 8, $header[$i], 1, 0, 'C', true); + } + $this->Ln(); + + // ================= DATA ================= + $this->SetFont('Arial', '', 10); + + foreach ($data as $row) { + + // hitung multiline + $nbDesc = $this->NbLines($columnWidths[2] * $scalingFactor, $row[2]); + $nbKet = $this->NbLines($columnWidths[3] * $scalingFactor, $row[3]); + + $maxHeight = 8 * max($nbDesc, $nbKet); + + // pagination + if ($this->GetY() + $maxHeight > $this->PageBreakTrigger) { + $this->AddPage(); + + $this->SetFillColor(255, 140, 0); + $this->SetFont('Arial', '', 10); + + for ($i = 0; $i < count($header); $i++) { + $this->Cell($columnWidths[$i] * $scalingFactor, 8, $header[$i], 1, 0, 'C', true); + } + $this->Ln(); + + $this->SetFont('Arial', '', 10); + } + + $x = $this->GetX(); + $y = $this->GetY(); + + // 1. No + $this->Cell($columnWidths[0] * $scalingFactor, $maxHeight, $row[0], 1, 0, 'C'); + + // 2. Tanggal + $this->Cell($columnWidths[1] * $scalingFactor, $maxHeight, $row[1], 1, 0, 'C'); + + // 3. Nama Item (multiline) + $this->SetXY($x + ($columnWidths[0] + $columnWidths[1]) * $scalingFactor, $y); + $this->MultiCell( + $columnWidths[2] * $scalingFactor, + $maxHeight / max(1, $nbDesc), + $row[2], + 1, + 'L' + ); + + // 4. Keterangan (multiline) + $this->SetXY($x + ($columnWidths[0] + $columnWidths[1] + $columnWidths[2]) * $scalingFactor, $y); + $this->MultiCell( + $columnWidths[3] * $scalingFactor, + $maxHeight / max(1, $nbKet), + $row[3], + 1, + 'L' + ); + + // 5. Qty + $this->SetXY($x + ($columnWidths[0] + $columnWidths[1] + $columnWidths[2] + $columnWidths[3]) * $scalingFactor, $y); + $this->Cell($columnWidths[4] * $scalingFactor, $maxHeight, $row[4], 1, 0, 'C'); + + // 6. Harga + $this->Cell($columnWidths[5] * $scalingFactor, $maxHeight, $row[5], 1, 0, 'R'); + + // 7. Subtotal + $this->Cell($columnWidths[6] * $scalingFactor, $maxHeight, $row[6], 1, 0, 'R'); + + $this->Ln($maxHeight); + } + } + + // Fungsi tambahan untuk menghitung jumlah baris Multicell + function NbLines($w, $txt) + { + // Hitung jumlah baris dalam MultiCell + $cw = &$this->CurrentFont['cw']; + if ($w == 0) { + $w = $this->w - $this->rMargin - $this->x; + } + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + if ($nb > 0 && $s[$nb - 1] == "\n") { + $nb--; + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + $c = $s[$i]; + if ($c == "\n") { + $i++; + $sep = -1; + $j = $i; + $l = 0; + $nl++; + continue; + } + if ($c == ' ') { + $sep = $i; + } + $l += $cw[$c]; + if ($l > $wmax) { + if ($sep == -1) { + if ($i == $j) { + $i++; + } + } else { + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + $nl++; + } else { + $i++; + } + } + return $nl; + } + + + function Total($subtotal,$lainnya,$totalAmount,$terbilang,$keteranganlain) + { + $this->Ln(1); + $this->SetFont('Arial', '', 11); + $this->Cell($this->maxWidth - 25, 9, 'Sub Total', 1, 0, 'R'); + $this->Cell(25, 9, $subtotal, 1, 1, 'R'); + $this->Cell($this->maxWidth - 25, 9, $keteranganlain, 1, 0, 'R'); + $this->Cell(25, 9, $lainnya, 1, 1, 'R'); + $this->SetFont('Arial', 'B', 11); + $this->Cell($this->maxWidth - 25, 9, 'Total', 1, 0, 'R'); + $this->Cell(25, 9, $totalAmount, 1, 1, 'R'); + $this->Ln(1); + $this->SetFont('Arial', 'I', 11); + $this->Cell(0, 9, 'Terbilang : ', 0, 1); + $this->SetFont('Arial', 'I', 11); + $this->MultiCell(190, 9, $terbilang, 1, 'L'); + } + + function Keterangan($Keterangan) + { + $this->Ln(10); + $this->SetFont('Arial', '', 11); + $this->SetFillColor(255, 140, 0); + $this->MultiCell(100, 6, $Keterangan, 0, 'L', true); + } + + function Pembuat($Pembuat) + { + $this->Ln(-25); + $this->SetFont('Arial', '', 11); + $this->Cell(300, 6, 'Mengetahui,', 0, 1, 'C'); + $this->Ln(18); + $this->SetFont('Arial', 'B', 11); + $this->Cell(300, 6, '( '.$Pembuat.' )', 0, 1, 'C'); + $this->SetFont('Arial', 'I', 11); + $this->Cell(300, 6, 'financial department', 0, 1, 'C'); + } +} + +?> diff --git a/application/libraries/index.html b/application/libraries/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/libraries/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/libraries/phpqrcode/CHANGELOG b/application/libraries/phpqrcode/CHANGELOG new file mode 100644 index 0000000..1088530 --- /dev/null +++ b/application/libraries/phpqrcode/CHANGELOG @@ -0,0 +1,38 @@ +* 1.0.0 build 2010031920 + + - first public release + - help in readme, install + - cleanup ans separation of QRtools and QRspec + - now TCPDF binding requires minimal changes in TCPDF, having most of job + done in QRtools tcpdfBarcodeArray + - nicer QRtools::timeBenchmark output + - license and copyright notices in files + - indent cleanup - from tab to 4spc, keep it that way please :) + - sf project, repository, wiki + - simple code generator in index.php + +* 1.1.0 build 2010032113 + + - added merge tool wich generate merged version of code + located in phpqrcode.php + - splited qrconst.php from qrlib.php + +* 1.1.1 build 2010032405 + + - patch by Rick Seymour allowing saving PNG and displaying it at the same time + - added version info in VERSION file + - modified merge tool to include version info into generated file + - fixed e-mail in almost all head comments + +* 1.1.2 build 2010032722 + + - full integration with TCPDF thanks to Nicola Asuni, it's author + - fixed bug with alphanumeric encoding detection + +* 1.1.3 build 2010081807 + + - short opening tags replaced with standard ones + +* 1.1.4 build 2010100721 + + - added missing static keyword QRinput::check (found by Luke Brookhart, Onjax LLC) diff --git a/application/libraries/phpqrcode/INSTALL b/application/libraries/phpqrcode/INSTALL new file mode 100644 index 0000000..eac6b07 --- /dev/null +++ b/application/libraries/phpqrcode/INSTALL @@ -0,0 +1,67 @@ +== REQUIREMENTS == + + * PHP5 + * PHP GD2 extension with JPEG and PNG support + +== INSTALLATION == + +If you want to recreate cache by yourself make sure cache directory is +writable and you have permisions to write into it. Also make sure you are +able to read files in it if you have cache option enabled + +== CONFIGURATION == + +Feel free to modify config constants in qrconfig.php file. Read about it in +provided comments and project wiki page (links in README file) + +== QUICK START == + +Notice: probably you should'nt use all of this in same script :) + +encode('PHP QR Code :)'); +QRspec::debug($tab, true); + +== TCPDF INTEGRATION == + +Inside bindings/tcpdf you will find slightly modified 2dbarcodes.php. +Instal phpqrcode liblaty inside tcpdf folder, then overwrite (or merge) +2dbarcodes.php + +Then use similar as example #50 from TCPDF examples: + + true, + 'padding' => 4, + 'fgcolor' => array(0,0,0), + 'bgcolor' => false, //array(255,255,255) +); + +//code name: QR, specify error correction level after semicolon (L,M,Q,H) +$pdf->write2DBarcode('PHP QR Code :)', 'QR,L', '', '', 30, 30, $style, 'N'); diff --git a/application/libraries/phpqrcode/LICENSE b/application/libraries/phpqrcode/LICENSE new file mode 100644 index 0000000..1883303 --- /dev/null +++ b/application/libraries/phpqrcode/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/application/libraries/phpqrcode/README b/application/libraries/phpqrcode/README new file mode 100644 index 0000000..a022fb5 --- /dev/null +++ b/application/libraries/phpqrcode/README @@ -0,0 +1,45 @@ +This is PHP implementation of QR Code 2-D barcode generator. It is pure-php +LGPL-licensed implementation based on C libqrencode by Kentaro Fukuchi. + +== LICENSING == + +Copyright (C) 2010 by Dominik Dzienia + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 3 of the License, or any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Lesser General Public License (LICENSE file) +for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +== INSTALATION AND USAGE == + + * INSTALL file + * http://sourceforge.net/apps/mediawiki/phpqrcode/index.php?title=Main_Page + +== CONTACT == + +Fell free to contact me via e-mail (deltalab at poczta dot fm) or using +folowing project pages: + + * http://sourceforge.net/projects/phpqrcode/ + * http://phpqrcode.sourceforge.net/ + +== ACKNOWLEDGMENTS == + +Based on C libqrencode library (ver. 3.1.1) +Copyright (C) 2006-2010 by Kentaro Fukuchi +http://megaui.net/fukuchi/works/qrencode/index.en.html + +QR Code is registered trademarks of DENSO WAVE INCORPORATED in JAPAN and other +countries. + +Reed-Solomon code encoder is written by Phil Karn, KA9Q. +Copyright (C) 2002, 2003, 2004, 2006 Phil Karn, KA9Q + \ No newline at end of file diff --git a/application/libraries/phpqrcode/VERSION b/application/libraries/phpqrcode/VERSION new file mode 100644 index 0000000..9f99279 --- /dev/null +++ b/application/libraries/phpqrcode/VERSION @@ -0,0 +1,2 @@ +1.1.4 +2010100721 \ No newline at end of file diff --git a/application/libraries/phpqrcode/bindings/tcpdf/qrcode.php b/application/libraries/phpqrcode/bindings/tcpdf/qrcode.php new file mode 100644 index 0000000..7995460 --- /dev/null +++ b/application/libraries/phpqrcode/bindings/tcpdf/qrcode.php @@ -0,0 +1,2875 @@ + +// http://phpqrcode.sourceforge.net/ +// https://sourceforge.net/projects/phpqrcode/ +// +// The "PHP QR Code encoder" is based on +// "C libqrencode library" (ver. 3.1.1) +// License: GNU-LGPL 2.1 +// Copyright (C) 2006-2010 by Kentaro Fukuchi +// http://megaui.net/fukuchi/works/qrencode/index.en.html +// +// Reed-Solomon code encoder is written by Phil Karn, KA9Q. +// Copyright (C) 2002-2006 Phil Karn, KA9Q +// +// QR Code is registered trademark of DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/index-e.html +// --------------------------------------------------------- +// +// Author: Nicola Asuni +// +// (c) Copyright 2010: +// Nicola Asuni +// Tecnick.com S.r.l. +// Via della Pace, 11 +// 09044 Quartucciu (CA) +// ITALY +// www.tecnick.com +// info@tecnick.com +//============================================================+ + +/** + * Class to create QR-code arrays for TCPDF class. + * QR Code symbol is a 2D barcode that can be scanned by handy terminals such as a mobile phone with CCD. + * The capacity of QR Code is up to 7000 digits or 4000 characters, and has high robustness. + * This class supports QR Code model 2, described in JIS (Japanese Industrial Standards) X0510:2004 or ISO/IEC 18004. + * Currently the following features are not supported: ECI and FNC1 mode, Micro QR Code, QR Code model 1, Structured mode. + * + * This class is derived from "PHP QR Code encoder" by Dominik Dzienia (http://phpqrcode.sourceforge.net/) based on "libqrencode C library 3.1.1." by Kentaro Fukuchi (http://megaui.net/fukuchi/works/qrencode/index.en.html), contains Reed-Solomon code written by Phil Karn, KA9Q. QR Code is registered trademark of DENSO WAVE INCORPORATED (http://www.denso-wave.com/qrcode/index-e.html). + * Please read comments on this class source file for full copyright and license information. + * + * @package com.tecnick.tcpdf + * @abstract Class for generating QR-code array for TCPDF. + * @author Nicola Asuni + * @copyright 2010 Nicola Asuni - Tecnick.com S.r.l (www.tecnick.com) Via Della Pace, 11 - 09044 - Quartucciu (CA) - ITALY - www.tecnick.com - info@tecnick.com + * @link http://www.tcpdf.org + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version 1.0.002 + */ + +// definitions +if (!defined('QRCODEDEFS')) { + + /** + * Indicate that definitions for this class are set + */ + define('QRCODEDEFS', true); + + // ----------------------------------------------------- + + // Encoding modes (characters which can be encoded in QRcode) + + /** + * Encoding mode + */ + define('QR_MODE_NL', -1); + + /** + * Encoding mode numeric (0-9). 3 characters are encoded to 10bit length. In theory, 7089 characters or less can be stored in a QRcode. + */ + define('QR_MODE_NM', 0); + + /** + * Encoding mode alphanumeric (0-9A-Z $%*+-./:) 45characters. 2 characters are encoded to 11bit length. In theory, 4296 characters or less can be stored in a QRcode. + */ + define('QR_MODE_AN', 1); + + /** + * Encoding mode 8bit byte data. In theory, 2953 characters or less can be stored in a QRcode. + */ + define('QR_MODE_8B', 2); + + /** + * Encoding mode KANJI. A KANJI character (multibyte character) is encoded to 13bit length. In theory, 1817 characters or less can be stored in a QRcode. + */ + define('QR_MODE_KJ', 3); + + /** + * Encoding mode STRUCTURED (currently unsupported) + */ + define('QR_MODE_ST', 4); + + // ----------------------------------------------------- + + // Levels of error correction. + // QRcode has a function of an error correcting for miss reading that white is black. + // Error correcting is defined in 4 level as below. + + /** + * Error correction level L : About 7% or less errors can be corrected. + */ + define('QR_ECLEVEL_L', 0); + + /** + * Error correction level M : About 15% or less errors can be corrected. + */ + define('QR_ECLEVEL_M', 1); + + /** + * Error correction level Q : About 25% or less errors can be corrected. + */ + define('QR_ECLEVEL_Q', 2); + + /** + * Error correction level H : About 30% or less errors can be corrected. + */ + define('QR_ECLEVEL_H', 3); + + // ----------------------------------------------------- + + // Version. Size of QRcode is defined as version. + // Version is from 1 to 40. + // Version 1 is 21*21 matrix. And 4 modules increases whenever 1 version increases. + // So version 40 is 177*177 matrix. + + /** + * Maximum QR Code version. + */ + define('QRSPEC_VERSION_MAX', 40); + + /** + * Maximum matrix size for maximum version (version 40 is 177*177 matrix). + */ + define('QRSPEC_WIDTH_MAX', 177); + + // ----------------------------------------------------- + + /** + * Matrix index to get width from $capacity array. + */ + define('QRCAP_WIDTH', 0); + + /** + * Matrix index to get number of words from $capacity array. + */ + define('QRCAP_WORDS', 1); + + /** + * Matrix index to get remainder from $capacity array. + */ + define('QRCAP_REMINDER', 2); + + /** + * Matrix index to get error correction level from $capacity array. + */ + define('QRCAP_EC', 3); + + // ----------------------------------------------------- + + // Structure (currently usupported) + + /** + * Number of header bits for structured mode + */ + define('STRUCTURE_HEADER_BITS', 20); + + /** + * Max number of symbols for structured mode + */ + define('MAX_STRUCTURED_SYMBOLS', 16); + + // ----------------------------------------------------- + + // Masks + + /** + * Down point base value for case 1 mask pattern (concatenation of same color in a line or a column) + */ + define('N1', 3); + + /** + * Down point base value for case 2 mask pattern (module block of same color) + */ + define('N2', 3); + + /** + * Down point base value for case 3 mask pattern (1:1:3:1:1(dark:bright:dark:bright:dark)pattern in a line or a column) + */ + define('N3', 40); + + /** + * Down point base value for case 4 mask pattern (ration of dark modules in whole) + */ + define('N4', 10); + + // ----------------------------------------------------- + + // Optimization settings + + /** + * if true, estimates best mask (spec. default, but extremally slow; set to false to significant performance boost but (propably) worst quality code + */ + define('QR_FIND_BEST_MASK', true); + + /** + * if false, checks all masks available, otherwise value tells count of masks need to be checked, mask id are got randomly + */ + define('QR_FIND_FROM_RANDOM', 2); + + /** + * when QR_FIND_BEST_MASK === false + */ + define('QR_DEFAULT_MASK', 2); + + // ----------------------------------------------------- + +} // end of definitions + +// #*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*# + +if (!class_exists('QRcode', false)) { + + // for compaibility with PHP4 + if (!function_exists('str_split')) { + /** + * Convert a string to an array (needed for PHP4 compatibility) + * @param string $string The input string. + * @param int $split_length Maximum length of the chunk. + * @return If the optional split_length parameter is specified, the returned array will be broken down into chunks with each being split_length in length, otherwise each chunk will be one character in length. FALSE is returned if split_length is less than 1. If the split_length length exceeds the length of string , the entire string is returned as the first (and only) array element. + */ + function str_split($string, $split_length=1) { + if ((strlen($string) > $split_length) OR (!$split_length)) { + do { + $c = strlen($string); + $parts[] = substr($string, 0, $split_length); + $string = substr($string, $split_length); + } while ($string !== false); + } else { + $parts = array($string); + } + return $parts; + } + } + + // ##################################################### + + /** + * Class to create QR-code arrays for TCPDF class. + * QR Code symbol is a 2D barcode that can be scanned by handy terminals such as a mobile phone with CCD. + * The capacity of QR Code is up to 7000 digits or 4000 characters, and has high robustness. + * This class supports QR Code model 2, described in JIS (Japanese Industrial Standards) X0510:2004 or ISO/IEC 18004. + * Currently the following features are not supported: ECI and FNC1 mode, Micro QR Code, QR Code model 1, Structured mode. + * + * This class is derived from "PHP QR Code encoder" by Dominik Dzienia (http://phpqrcode.sourceforge.net/) based on "libqrencode C library 3.1.1." by Kentaro Fukuchi (http://megaui.net/fukuchi/works/qrencode/index.en.html), contains Reed-Solomon code written by Phil Karn, KA9Q. QR Code is registered trademark of DENSO WAVE INCORPORATED (http://www.denso-wave.com/qrcode/index-e.html). + * Please read comments on this class source file for full copyright and license information. + * + * @name QRcode + * @package com.tecnick.tcpdf + * @abstract Class for generating QR-code array for TCPDF. + * @author Nicola Asuni + * @copyright 2010 Nicola Asuni - Tecnick.com S.r.l (www.tecnick.com) Via Della Pace, 11 - 09044 - Quartucciu (CA) - ITALY - www.tecnick.com - info@tecnick.com + * @link http://www.tcpdf.org + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @version 1.0.002 + */ + class QRcode { + + /** + * @var barcode array to be returned which is readable by TCPDF + * @access protected + */ + protected $barcode_array = array(); + + /** + * @var QR code version. Size of QRcode is defined as version. Version is from 1 to 40. Version 1 is 21*21 matrix. And 4 modules increases whenever 1 version increases. So version 40 is 177*177 matrix. + * @access protected + */ + protected $version = 0; + + /** + * @var Levels of error correction. See definitions for possible values. + * @access protected + */ + protected $level = QR_ECLEVEL_L; + + /** + * @var Encoding mode + * @access protected + */ + protected $hint = QR_MODE_8B; + + /** + * @var if true the input string will be converted to uppercase + * @access protected + */ + protected $casesensitive = true; + + /** + * @var structured QR code (not supported yet) + * @access protected + */ + protected $structured = 0; + + /** + * @var mask data + * @access protected + */ + protected $data; + + // FrameFiller + + /** + * @var width + * @access protected + */ + protected $width; + + /** + * @var frame + * @access protected + */ + protected $frame; + + /** + * @var X position of bit + * @access protected + */ + protected $x; + + /** + * @var Y position of bit + * @access protected + */ + protected $y; + + /** + * @var direction + * @access protected + */ + protected $dir; + + /** + * @var single bit + * @access protected + */ + protected $bit; + + // ---- QRrawcode ---- + + /** + * @var data code + * @access protected + */ + protected $datacode = array(); + + /** + * @var error correction code + * @access protected + */ + protected $ecccode = array(); + + /** + * @var blocks + * @access protected + */ + protected $blocks; + + /** + * @var Reed-Solomon blocks + * @access protected + */ + protected $rsblocks = array(); //of RSblock + + /** + * @var counter + * @access protected + */ + protected $count; + + /** + * @var data length + * @access protected + */ + protected $dataLength; + + /** + * @var error correction length + * @access protected + */ + protected $eccLength; + + /** + * @var b1 + * @access protected + */ + protected $b1; + + // ---- QRmask ---- + + /** + * @var run length + * @access protected + */ + protected $runLength = array(); + + // ---- QRsplit ---- + + /** + * @var input data string + * @access protected + */ + protected $dataStr = ''; + + /** + * @var input items + * @access protected + */ + protected $items; + + // Reed-Solomon items + + /** + * @var Reed-Solomon items + * @access protected + */ + protected $rsitems = array(); + + /** + * @var array of frames + * @access protected + */ + protected $frames = array(); + + /** + * @var alphabet-numeric convesion table + * @access protected + */ + protected $anTable = array( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 // + ); + + /** + * @var array Table of the capacity of symbols + * See Table 1 (pp.13) and Table 12-16 (pp.30-36), JIS X0510:2004. + * @access protected + */ + protected $capacity = array( + array( 0, 0, 0, array( 0, 0, 0, 0)), // + array( 21, 26, 0, array( 7, 10, 13, 17)), // 1 + array( 25, 44, 7, array( 10, 16, 22, 28)), // + array( 29, 70, 7, array( 15, 26, 36, 44)), // + array( 33, 100, 7, array( 20, 36, 52, 64)), // + array( 37, 134, 7, array( 26, 48, 72, 88)), // 5 + array( 41, 172, 7, array( 36, 64, 96, 112)), // + array( 45, 196, 0, array( 40, 72, 108, 130)), // + array( 49, 242, 0, array( 48, 88, 132, 156)), // + array( 53, 292, 0, array( 60, 110, 160, 192)), // + array( 57, 346, 0, array( 72, 130, 192, 224)), // 10 + array( 61, 404, 0, array( 80, 150, 224, 264)), // + array( 65, 466, 0, array( 96, 176, 260, 308)), // + array( 69, 532, 0, array( 104, 198, 288, 352)), // + array( 73, 581, 3, array( 120, 216, 320, 384)), // + array( 77, 655, 3, array( 132, 240, 360, 432)), // 15 + array( 81, 733, 3, array( 144, 280, 408, 480)), // + array( 85, 815, 3, array( 168, 308, 448, 532)), // + array( 89, 901, 3, array( 180, 338, 504, 588)), // + array( 93, 991, 3, array( 196, 364, 546, 650)), // + array( 97, 1085, 3, array( 224, 416, 600, 700)), // 20 + array(101, 1156, 4, array( 224, 442, 644, 750)), // + array(105, 1258, 4, array( 252, 476, 690, 816)), // + array(109, 1364, 4, array( 270, 504, 750, 900)), // + array(113, 1474, 4, array( 300, 560, 810, 960)), // + array(117, 1588, 4, array( 312, 588, 870, 1050)), // 25 + array(121, 1706, 4, array( 336, 644, 952, 1110)), // + array(125, 1828, 4, array( 360, 700, 1020, 1200)), // + array(129, 1921, 3, array( 390, 728, 1050, 1260)), // + array(133, 2051, 3, array( 420, 784, 1140, 1350)), // + array(137, 2185, 3, array( 450, 812, 1200, 1440)), // 30 + array(141, 2323, 3, array( 480, 868, 1290, 1530)), // + array(145, 2465, 3, array( 510, 924, 1350, 1620)), // + array(149, 2611, 3, array( 540, 980, 1440, 1710)), // + array(153, 2761, 3, array( 570, 1036, 1530, 1800)), // + array(157, 2876, 0, array( 570, 1064, 1590, 1890)), // 35 + array(161, 3034, 0, array( 600, 1120, 1680, 1980)), // + array(165, 3196, 0, array( 630, 1204, 1770, 2100)), // + array(169, 3362, 0, array( 660, 1260, 1860, 2220)), // + array(173, 3532, 0, array( 720, 1316, 1950, 2310)), // + array(177, 3706, 0, array( 750, 1372, 2040, 2430)) // 40 + ); + + /** + * @var array Length indicator + * @access protected + */ + protected $lengthTableBits = array( + array(10, 12, 14), + array( 9, 11, 13), + array( 8, 16, 16), + array( 8, 10, 12) + ); + + /** + * @var array Table of the error correction code (Reed-Solomon block) + * See Table 12-16 (pp.30-36), JIS X0510:2004. + * @access protected + */ + protected $eccTable = array( + array(array( 0, 0), array( 0, 0), array( 0, 0), array( 0, 0)), // + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), // 1 + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), // + array(array( 1, 0), array( 1, 0), array( 2, 0), array( 2, 0)), // + array(array( 1, 0), array( 2, 0), array( 2, 0), array( 4, 0)), // + array(array( 1, 0), array( 2, 0), array( 2, 2), array( 2, 2)), // 5 + array(array( 2, 0), array( 4, 0), array( 4, 0), array( 4, 0)), // + array(array( 2, 0), array( 4, 0), array( 2, 4), array( 4, 1)), // + array(array( 2, 0), array( 2, 2), array( 4, 2), array( 4, 2)), // + array(array( 2, 0), array( 3, 2), array( 4, 4), array( 4, 4)), // + array(array( 2, 2), array( 4, 1), array( 6, 2), array( 6, 2)), // 10 + array(array( 4, 0), array( 1, 4), array( 4, 4), array( 3, 8)), // + array(array( 2, 2), array( 6, 2), array( 4, 6), array( 7, 4)), // + array(array( 4, 0), array( 8, 1), array( 8, 4), array(12, 4)), // + array(array( 3, 1), array( 4, 5), array(11, 5), array(11, 5)), // + array(array( 5, 1), array( 5, 5), array( 5, 7), array(11, 7)), // 15 + array(array( 5, 1), array( 7, 3), array(15, 2), array( 3, 13)), // + array(array( 1, 5), array(10, 1), array( 1, 15), array( 2, 17)), // + array(array( 5, 1), array( 9, 4), array(17, 1), array( 2, 19)), // + array(array( 3, 4), array( 3, 11), array(17, 4), array( 9, 16)), // + array(array( 3, 5), array( 3, 13), array(15, 5), array(15, 10)), // 20 + array(array( 4, 4), array(17, 0), array(17, 6), array(19, 6)), // + array(array( 2, 7), array(17, 0), array( 7, 16), array(34, 0)), // + array(array( 4, 5), array( 4, 14), array(11, 14), array(16, 14)), // + array(array( 6, 4), array( 6, 14), array(11, 16), array(30, 2)), // + array(array( 8, 4), array( 8, 13), array( 7, 22), array(22, 13)), // 25 + array(array(10, 2), array(19, 4), array(28, 6), array(33, 4)), // + array(array( 8, 4), array(22, 3), array( 8, 26), array(12, 28)), // + array(array( 3, 10), array( 3, 23), array( 4, 31), array(11, 31)), // + array(array( 7, 7), array(21, 7), array( 1, 37), array(19, 26)), // + array(array( 5, 10), array(19, 10), array(15, 25), array(23, 25)), // 30 + array(array(13, 3), array( 2, 29), array(42, 1), array(23, 28)), // + array(array(17, 0), array(10, 23), array(10, 35), array(19, 35)), // + array(array(17, 1), array(14, 21), array(29, 19), array(11, 46)), // + array(array(13, 6), array(14, 23), array(44, 7), array(59, 1)), // + array(array(12, 7), array(12, 26), array(39, 14), array(22, 41)), // 35 + array(array( 6, 14), array( 6, 34), array(46, 10), array( 2, 64)), // + array(array(17, 4), array(29, 14), array(49, 10), array(24, 46)), // + array(array( 4, 18), array(13, 32), array(48, 14), array(42, 32)), // + array(array(20, 4), array(40, 7), array(43, 22), array(10, 67)), // + array(array(19, 6), array(18, 31), array(34, 34), array(20, 61)) // 40 + ); + + /** + * @var array Positions of alignment patterns. + * This array includes only the second and the third position of the alignment patterns. Rest of them can be calculated from the distance between them. + * See Table 1 in Appendix E (pp.71) of JIS X0510:2004. + * @access protected + */ + protected $alignmentPattern = array( + array( 0, 0), + array( 0, 0), array(18, 0), array(22, 0), array(26, 0), array(30, 0), // 1- 5 + array(34, 0), array(22, 38), array(24, 42), array(26, 46), array(28, 50), // 6-10 + array(30, 54), array(32, 58), array(34, 62), array(26, 46), array(26, 48), // 11-15 + array(26, 50), array(30, 54), array(30, 56), array(30, 58), array(34, 62), // 16-20 + array(28, 50), array(26, 50), array(30, 54), array(28, 54), array(32, 58), // 21-25 + array(30, 58), array(34, 62), array(26, 50), array(30, 54), array(26, 52), // 26-30 + array(30, 56), array(34, 60), array(30, 58), array(34, 62), array(30, 54), // 31-35 + array(24, 50), array(28, 54), array(32, 58), array(26, 54), array(30, 58) // 35-40 + ); + + /** + * @var array Version information pattern (BCH coded). + * See Table 1 in Appendix D (pp.68) of JIS X0510:2004. + * size: [QRSPEC_VERSION_MAX - 6] + * @access protected + */ + protected $versionPattern = array( + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, // + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, // + 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, // + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, // + 0x27541, 0x28c69 + ); + + /** + * @var array Format information + * @access protected + */ + protected $formatInfo = array( + array(0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976), // + array(0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0), // + array(0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed), // + array(0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b) // + ); + + + // ------------------------------------------------- + // ------------------------------------------------- + + + /** + * This is the class constructor. + * Creates a QRcode object + * @param string $code code to represent using QRcode + * @param string $eclevel error level:
  • L : About 7% or less errors can be corrected.
  • M : About 15% or less errors can be corrected.
  • Q : About 25% or less errors can be corrected.
  • H : About 30% or less errors can be corrected.
+ * @access public + * @since 1.0.000 + */ + public function __construct($code, $eclevel = 'L') { + $barcode_array = array(); + if ((is_null($code)) OR ($code == '\0') OR ($code == '')) { + return false; + } + // set error correction level + $this->level = array_search($eclevel, array('L', 'M', 'Q', 'H')); + if ($this->level === false) { + $this->level = QR_ECLEVEL_L; + } + if (($this->hint != QR_MODE_8B) AND ($this->hint != QR_MODE_KJ)) { + return false; + } + if (($this->version < 0) OR ($this->version > QRSPEC_VERSION_MAX)) { + return false; + } + $this->items = array(); + $this->encodeString($code); + $qrTab = $this->binarize($this->data); + $size = count($qrTab); + $barcode_array['num_rows'] = $size; + $barcode_array['num_cols'] = $size; + $barcode_array['bcode'] = array(); + foreach ($qrTab as $line) { + $arrAdd = array(); + foreach (str_split($line) as $char) { + $arrAdd[] = ($char=='1')?1:0; + } + $barcode_array['bcode'][] = $arrAdd; + } + $this->barcode_array = $barcode_array; + } + + /** + * Returns a barcode array which is readable by TCPDF + * @return array barcode array readable by TCPDF; + * @access public + */ + public function getBarcodeArray() { + return $this->barcode_array; + } + + /** + * Convert the frame in binary form + * @param array $frame array to binarize + * @return array frame in binary form + */ + protected function binarize($frame) { + $len = count($frame); + // the frame is square (width = height) + foreach ($frame as &$frameLine) { + for ($i=0; $i<$len; $i++) { + $frameLine[$i] = (ord($frameLine[$i])&1)?'1':'0'; + } + } + return $frame; + } + + /** + * Encode the input string to QR code + * @param string $string input string to encode + */ + protected function encodeString($string) { + $this->dataStr = $string; + if (!$this->casesensitive) { + $this->toUpper(); + } + $ret = $this->splitString(); + if ($ret < 0) { + return NULL; + } + $this->encodeMask(-1); + } + + /** + * Encode mask + * @param int $mask masking mode + */ + protected function encodeMask($mask) { + $spec = array(0, 0, 0, 0, 0); + $this->datacode = $this->getByteStream($this->items); + if (is_null($this->datacode)) { + return NULL; + } + $spec = $this->getEccSpec($this->version, $this->level, $spec); + $this->b1 = $this->rsBlockNum1($spec); + $this->dataLength = $this->rsDataLength($spec); + $this->eccLength = $this->rsEccLength($spec); + $this->ecccode = array_fill(0, $this->eccLength, 0); + $this->blocks = $this->rsBlockNum($spec); + $ret = $this->init($spec); + if ($ret < 0) { + return NULL; + } + $this->count = 0; + $this->width = $this->getWidth($this->version); + $this->frame = $this->newFrame($this->version); + $this->x = $this->width - 1; + $this->y = $this->width - 1; + $this->dir = -1; + $this->bit = -1; + // inteleaved data and ecc codes + for ($i=0; $i < ($this->dataLength + $this->eccLength); $i++) { + $code = $this->getCode(); + $bit = 0x80; + for ($j=0; $j<8; $j++) { + $addr = $this->getNextPosition(); + $this->setFrameAt($addr, 0x02 | (($bit & $code) != 0)); + $bit = $bit >> 1; + } + } + // remainder bits + $j = $this->getRemainder($this->version); + for ($i=0; $i<$j; $i++) { + $addr = $this->getNextPosition(); + $this->setFrameAt($addr, 0x02); + } + // masking + $this->runLength = array_fill(0, QRSPEC_WIDTH_MAX + 1, 0); + if ($mask < 0) { + if (QR_FIND_BEST_MASK) { + $masked = $this->mask($this->width, $this->frame, $this->level); + } else { + $masked = $this->makeMask($this->width, $this->frame, (intval(QR_DEFAULT_MASK) % 8), $this->level); + } + } else { + $masked = $this->makeMask($this->width, $this->frame, $mask, $this->level); + } + if ($masked == NULL) { + return NULL; + } + $this->data = $masked; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // FrameFiller + + /** + * Set frame value at specified position + * @param array $at x,y position + * @param int $val value of the character to set + */ + protected function setFrameAt($at, $val) { + $this->frame[$at['y']][$at['x']] = chr($val); + } + + /** + * Get frame value at specified position + * @param array $at x,y position + * @return value at specified position + */ + protected function getFrameAt($at) { + return ord($this->frame[$at['y']][$at['x']]); + } + + /** + * Return the next frame position + * @return array of x,y coordinates + */ + protected function getNextPosition() { + do { + if ($this->bit == -1) { + $this->bit = 0; + return array('x'=>$this->x, 'y'=>$this->y); + } + $x = $this->x; + $y = $this->y; + $w = $this->width; + if ($this->bit == 0) { + $x--; + $this->bit++; + } else { + $x++; + $y += $this->dir; + $this->bit--; + } + if ($this->dir < 0) { + if ($y < 0) { + $y = 0; + $x -= 2; + $this->dir = 1; + if ($x == 6) { + $x--; + $y = 9; + } + } + } else { + if ($y == $w) { + $y = $w - 1; + $x -= 2; + $this->dir = -1; + if ($x == 6) { + $x--; + $y -= 8; + } + } + } + if (($x < 0) OR ($y < 0)) { + return NULL; + } + $this->x = $x; + $this->y = $y; + } while(ord($this->frame[$y][$x]) & 0x80); + return array('x'=>$x, 'y'=>$y); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRrawcode + + /** + * Initialize code. + * @param array $spec array of ECC specification + * @return 0 in case of success, -1 in case of error + */ + protected function init($spec) { + $dl = $this->rsDataCodes1($spec); + $el = $this->rsEccCodes1($spec); + $rs = $this->init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + $blockNo = 0; + $dataPos = 0; + $eccPos = 0; + $endfor = $this->rsBlockNum1($spec); + for ($i=0; $i < $endfor; ++$i) { + $ecc = array_slice($this->ecccode, $eccPos); + $this->rsblocks[$blockNo] = array(); + $this->rsblocks[$blockNo]['dataLength'] = $dl; + $this->rsblocks[$blockNo]['data'] = array_slice($this->datacode, $dataPos); + $this->rsblocks[$blockNo]['eccLength'] = $el; + $ecc = $this->encode_rs_char($rs, $this->rsblocks[$blockNo]['data'], $ecc); + $this->rsblocks[$blockNo]['ecc'] = $ecc; + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + if ($this->rsBlockNum2($spec) == 0) { + return 0; + } + $dl = $this->rsDataCodes2($spec); + $el = $this->rsEccCodes2($spec); + $rs = $this->init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + if ($rs == NULL) { + return -1; + } + $endfor = $this->rsBlockNum2($spec); + for ($i=0; $i < $endfor; ++$i) { + $ecc = array_slice($this->ecccode, $eccPos); + $this->rsblocks[$blockNo] = array(); + $this->rsblocks[$blockNo]['dataLength'] = $dl; + $this->rsblocks[$blockNo]['data'] = array_slice($this->datacode, $dataPos); + $this->rsblocks[$blockNo]['eccLength'] = $el; + $ecc = $this->encode_rs_char($rs, $this->rsblocks[$blockNo]['data'], $ecc); + $this->rsblocks[$blockNo]['ecc'] = $ecc; + $this->ecccode = array_merge(array_slice($this->ecccode, 0, $eccPos), $ecc); + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + return 0; + } + + /** + * Return Reed-Solomon block code. + * @return array rsblocks + */ + protected function getCode() { + if ($this->count < $this->dataLength) { + $row = $this->count % $this->blocks; + $col = $this->count / $this->blocks; + if ($col >= $this->rsblocks[0]['dataLength']) { + $row += $this->b1; + } + $ret = $this->rsblocks[$row]['data'][$col]; + } elseif ($this->count < $this->dataLength + $this->eccLength) { + $row = ($this->count - $this->dataLength) % $this->blocks; + $col = ($this->count - $this->dataLength) / $this->blocks; + $ret = $this->rsblocks[$row]['ecc'][$col]; + } else { + return 0; + } + $this->count++; + return $ret; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRmask + + /** + * Write Format Information on frame and returns the number of black bits + * @param int $width frame width + * @param array $frame frame + * @param array $mask masking mode + * @param int $level error correction level + * @return int blacks + */ + protected function writeFormatInformation($width, &$frame, $mask, $level) { + $blacks = 0; + $format = $this->getFormatInfo($mask, $level); + for ($i=0; $i<8; ++$i) { + if ($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + $frame[8][$width - 1 - $i] = chr($v); + if ($i < 6) { + $frame[$i][8] = chr($v); + } else { + $frame[$i + 1][8] = chr($v); + } + $format = $format >> 1; + } + for ($i=0; $i<7; ++$i) { + if ($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + $frame[$width - 7 + $i][8] = chr($v); + if ($i == 0) { + $frame[8][7] = chr($v); + } else { + $frame[8][6 - $i] = chr($v); + } + $format = $format >> 1; + } + return $blacks; + } + + /** + * mask0 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask0($x, $y) { + return ($x + $y) & 1; + } + + /** + * mask1 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask1($x, $y) { + return ($y & 1); + } + + /** + * mask2 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask2($x, $y) { + return ($x % 3); + } + + /** + * mask3 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask3($x, $y) { + return ($x + $y) % 3; + } + + /** + * mask4 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask4($x, $y) { + return (((int)($y / 2)) + ((int)($x / 3))) & 1; + } + + /** + * mask5 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask5($x, $y) { + return (($x * $y) & 1) + ($x * $y) % 3; + } + + /** + * mask6 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask6($x, $y) { + return ((($x * $y) & 1) + ($x * $y) % 3) & 1; + } + + /** + * mask7 + * @param int $x X position + * @param int $y Y position + * @return int mask + */ + protected function mask7($x, $y) { + return ((($x * $y) % 3) + (($x + $y) & 1)) & 1; + } + + /** + * Return bitmask + * @param int $maskNo mask number + * @param int $width width + * @param array $frame frame + * @return array bitmask + */ + protected function generateMaskNo($maskNo, $width, $frame) { + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + for ($y=0; $y<$width; ++$y) { + for ($x=0; $x<$width; ++$x) { + if (ord($frame[$y][$x]) & 0x80) { + $bitMask[$y][$x] = 0; + } else { + $maskFunc = call_user_func(array($this, 'mask'.$maskNo), $x, $y); + $bitMask[$y][$x] = ($maskFunc == 0)?1:0; + } + } + } + return $bitMask; + } + + /** + * makeMaskNo + * @param int $maskNo + * @param int $width + * @param int $s + * @param int $d + * @param boolean $maskGenOnly + * @return int b + */ + protected function makeMaskNo($maskNo, $width, $s, &$d, $maskGenOnly=false) { + $b = 0; + $bitMask = array(); + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + if ($maskGenOnly) { + return; + } + $d = $s; + for ($y=0; $y<$width; ++$y) { + for ($x=0; $x<$width; ++$x) { + if ($bitMask[$y][$x] == 1) { + $d[$y][$x] = chr(ord($s[$y][$x]) ^ (int)$bitMask[$y][$x]); + } + $b += (int)(ord($d[$y][$x]) & 1); + } + } + return $b; + } + + /** + * makeMask + * @param int $width + * @param array $frame + * @param int $maskNo + * @param int $level + * @return array mask + */ + protected function makeMask($width, $frame, $maskNo, $level) { + $masked = array_fill(0, $width, str_repeat("\0", $width)); + $this->makeMaskNo($maskNo, $width, $frame, $masked); + $this->writeFormatInformation($width, $masked, $maskNo, $level); + return $masked; + } + + /** + * calcN1N3 + * @param int $length + * @return int demerit + */ + protected function calcN1N3($length) { + $demerit = 0; + for ($i=0; $i<$length; ++$i) { + if ($this->runLength[$i] >= 5) { + $demerit += (N1 + ($this->runLength[$i] - 5)); + } + if ($i & 1) { + if (($i >= 3) AND ($i < ($length-2)) AND ($this->runLength[$i] % 3 == 0)) { + $fact = (int)($this->runLength[$i] / 3); + if (($this->runLength[$i-2] == $fact) + AND ($this->runLength[$i-1] == $fact) + AND ($this->runLength[$i+1] == $fact) + AND ($this->runLength[$i+2] == $fact)) { + if (($this->runLength[$i-3] < 0) OR ($this->runLength[$i-3] >= (4 * $fact))) { + $demerit += N3; + } elseif ((($i+3) >= $length) OR ($this->runLength[$i+3] >= (4 * $fact))) { + $demerit += N3; + } + } + } + } + } + return $demerit; + } + + /** + * evaluateSymbol + * @param int $width + * @param array $frame + * @return int demerit + */ + protected function evaluateSymbol($width, $frame) { + $head = 0; + $demerit = 0; + for ($y=0; $y<$width; ++$y) { + $head = 0; + $this->runLength[0] = 1; + $frameY = $frame[$y]; + if ($y > 0) { + $frameYM = $frame[$y-1]; + } + for ($x=0; $x<$width; ++$x) { + if (($x > 0) AND ($y > 0)) { + $b22 = ord($frameY[$x]) & ord($frameY[$x-1]) & ord($frameYM[$x]) & ord($frameYM[$x-1]); + $w22 = ord($frameY[$x]) | ord($frameY[$x-1]) | ord($frameYM[$x]) | ord($frameYM[$x-1]); + if (($b22 | ($w22 ^ 1)) & 1) { + $demerit += N2; + } + } + if (($x == 0) AND (ord($frameY[$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } elseif ($x > 0) { + if ((ord($frameY[$x]) ^ ord($frameY[$x-1])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + $demerit += $this->calcN1N3($head+1); + } + for ($x=0; $x<$width; ++$x) { + $head = 0; + $this->runLength[0] = 1; + for ($y=0; $y<$width; ++$y) { + if (($y == 0) AND (ord($frame[$y][$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } elseif ($y > 0) { + if ((ord($frame[$y][$x]) ^ ord($frame[$y-1][$x])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + $demerit += $this->calcN1N3($head+1); + } + return $demerit; + } + + /** + * mask + * @param int $width + * @param array $frame + * @param int $level + * @return array best mask + */ + protected function mask($width, $frame, $level) { + $minDemerit = PHP_INT_MAX; + $bestMaskNum = 0; + $bestMask = array(); + $checked_masks = array(0, 1, 2, 3, 4, 5, 6, 7); + if (QR_FIND_FROM_RANDOM !== false) { + $howManuOut = 8 - (QR_FIND_FROM_RANDOM % 9); + for ($i = 0; $i < $howManuOut; ++$i) { + $remPos = rand (0, count($checked_masks)-1); + unset($checked_masks[$remPos]); + $checked_masks = array_values($checked_masks); + } + } + $bestMask = $frame; + foreach ($checked_masks as $i) { + $mask = array_fill(0, $width, str_repeat("\0", $width)); + $demerit = 0; + $blacks = 0; + $blacks = $this->makeMaskNo($i, $width, $frame, $mask); + $blacks += $this->writeFormatInformation($width, $mask, $i, $level); + $blacks = (int)(100 * $blacks / ($width * $width)); + $demerit = (int)((int)(abs($blacks - 50) / 5) * N4); + $demerit += $this->evaluateSymbol($width, $mask); + if ($demerit < $minDemerit) { + $minDemerit = $demerit; + $bestMask = $mask; + $bestMaskNum = $i; + } + } + return $bestMask; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRsplit + + /** + * Return true if the character at specified position is a number + * @param string $str string + * @param int $pos characted position + * @return boolean true of false + */ + protected function isdigitat($str, $pos) { + if ($pos >= strlen($str)) { + return false; + } + return ((ord($str[$pos]) >= ord('0'))&&(ord($str[$pos]) <= ord('9'))); + } + + /** + * Return true if the character at specified position is an alphanumeric character + * @param string $str string + * @param int $pos characted position + * @return boolean true of false + */ + protected function isalnumat($str, $pos) { + if ($pos >= strlen($str)) { + return false; + } + return ($this->lookAnTable(ord($str[$pos])) >= 0); + } + + /** + * identifyMode + * @param int $pos + * @return int mode + */ + protected function identifyMode($pos) { + if ($pos >= strlen($this->dataStr)) { + return QR_MODE_NL; + } + $c = $this->dataStr[$pos]; + if ($this->isdigitat($this->dataStr, $pos)) { + return QR_MODE_NM; + } elseif ($this->isalnumat($this->dataStr, $pos)) { + return QR_MODE_AN; + } elseif ($this->hint == QR_MODE_KJ) { + if ($pos+1 < strlen($this->dataStr)) { + $d = $this->dataStr[$pos+1]; + $word = (ord($c) << 8) | ord($d); + if (($word >= 0x8140 && $word <= 0x9ffc) OR ($word >= 0xe040 && $word <= 0xebbf)) { + return QR_MODE_KJ; + } + } + } + return QR_MODE_8B; + } + + /** + * eatNum + * @return int run + */ + protected function eatNum() { + $ln = $this->lengthIndicator(QR_MODE_NM, $this->version); + $p = 0; + while($this->isdigitat($this->dataStr, $p)) { + $p++; + } + $run = $p; + $mode = $this->identifyMode($p); + if ($mode == QR_MODE_8B) { + $dif = $this->estimateBitsModeNum($run) + 4 + $ln + + $this->estimateBitsMode8(1) // + 4 + l8 + - $this->estimateBitsMode8($run + 1); // - 4 - l8 + if ($dif > 0) { + return $this->eat8(); + } + } + if ($mode == QR_MODE_AN) { + $dif = $this->estimateBitsModeNum($run) + 4 + $ln + + $this->estimateBitsModeAn(1) // + 4 + la + - $this->estimateBitsModeAn($run + 1);// - 4 - la + if ($dif > 0) { + return $this->eatAn(); + } + } + $this->items = $this->appendNewInputItem($this->items, QR_MODE_NM, $run, str_split($this->dataStr)); + return $run; + } + + /** + * eatAn + * @return int run + */ + protected function eatAn() { + $la = $this->lengthIndicator(QR_MODE_AN, $this->version); + $ln = $this->lengthIndicator(QR_MODE_NM, $this->version); + $p = 0; + while($this->isalnumat($this->dataStr, $p)) { + if ($this->isdigitat($this->dataStr, $p)) { + $q = $p; + while($this->isdigitat($this->dataStr, $q)) { + $q++; + } + $dif = $this->estimateBitsModeAn($p) // + 4 + la + + $this->estimateBitsModeNum($q - $p) + 4 + $ln + - $this->estimateBitsModeAn($q); // - 4 - la + if ($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + $run = $p; + if (!$this->isalnumat($this->dataStr, $p)) { + $dif = $this->estimateBitsModeAn($run) + 4 + $la + + $this->estimateBitsMode8(1) // + 4 + l8 + - $this->estimateBitsMode8($run + 1); // - 4 - l8 + if ($dif > 0) { + return $this->eat8(); + } + } + $this->items = $this->appendNewInputItem($this->items, QR_MODE_AN, $run, str_split($this->dataStr)); + return $run; + } + + /** + * eatKanji + * @return int run + */ + protected function eatKanji() { + $p = 0; + while($this->identifyMode($p) == QR_MODE_KJ) { + $p += 2; + } + $this->items = $this->appendNewInputItem($this->items, QR_MODE_KJ, $p, str_split($this->dataStr)); + return $run; + } + + /** + * eat8 + * @return int run + */ + protected function eat8() { + $la = $this->lengthIndicator(QR_MODE_AN, $this->version); + $ln = $this->lengthIndicator(QR_MODE_NM, $this->version); + $p = 1; + $dataStrLen = strlen($this->dataStr); + while($p < $dataStrLen) { + $mode = $this->identifyMode($p); + if ($mode == QR_MODE_KJ) { + break; + } + if ($mode == QR_MODE_NM) { + $q = $p; + while($this->isdigitat($this->dataStr, $q)) { + $q++; + } + $dif = $this->estimateBitsMode8($p) // + 4 + l8 + + $this->estimateBitsModeNum($q - $p) + 4 + $ln + - $this->estimateBitsMode8($q); // - 4 - l8 + if ($dif < 0) { + break; + } else { + $p = $q; + } + } elseif ($mode == QR_MODE_AN) { + $q = $p; + while($this->isalnumat($this->dataStr, $q)) { + $q++; + } + $dif = $this->estimateBitsMode8($p) // + 4 + l8 + + $this->estimateBitsModeAn($q - $p) + 4 + $la + - $this->estimateBitsMode8($q); // - 4 - l8 + if ($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + $run = $p; + $this->items = $this->appendNewInputItem($this->items, QR_MODE_8B, $run, str_split($this->dataStr)); + return $run; + } + + /** + * splitString + */ + protected function splitString() { + while (strlen($this->dataStr) > 0) { + if ($this->dataStr == '') { + return 0; + } + $mode = $this->identifyMode(0); + switch ($mode) { + case QR_MODE_NM: { + $length = $this->eatNum(); + break; + } + case QR_MODE_AN: { + $length = $this->eatAn(); + break; + } + case QR_MODE_KJ: { + if ($hint == QR_MODE_KJ) { + $length = $this->eatKanji(); + } else { + $length = $this->eat8(); + } + break; + } + default: { + $length = $this->eat8(); + break; + } + } + if ($length == 0) { + return 0; + } + if ($length < 0) { + return -1; + } + $this->dataStr = substr($this->dataStr, $length); + } + } + + /** + * toUpper + */ + protected function toUpper() { + $stringLen = strlen($this->dataStr); + $p = 0; + while ($p < $stringLen) { + $mode = $this->identifyMode(substr($this->dataStr, $p), $this->hint); + if ($mode == QR_MODE_KJ) { + $p += 2; + } else { + if ((ord($this->dataStr[$p]) >= ord('a')) AND (ord($this->dataStr[$p]) <= ord('z'))) { + $this->dataStr[$p] = chr(ord($this->dataStr[$p]) - 32); + } + $p++; + } + } + return $this->dataStr; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRinputItem + + /** + * newInputItem + * @param int $mode + * @param int $size + * @param array $data + * @param array $bstream + * @return array input item + */ + protected function newInputItem($mode, $size, $data, $bstream=null) { + $setData = array_slice($data, 0, $size); + if (count($setData) < $size) { + $setData = array_merge($setData, array_fill(0, ($size - count($setData)), 0)); + } + if (!$this->check($mode, $size, $setData)) { + return NULL; + } + $inputitem = array(); + $inputitem['mode'] = $mode; + $inputitem['size'] = $size; + $inputitem['data'] = $setData; + $inputitem['bstream'] = $bstream; + return $inputitem; + } + + /** + * encodeModeNum + * @param array $inputitem + * @param int $version + * @return array input item + */ + protected function encodeModeNum($inputitem, $version) { + $words = (int)($inputitem['size'] / 3); + $inputitem['bstream'] = array(); + $val = 0x1; + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, $val); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], $this->lengthIndicator(QR_MODE_NM, $version), $inputitem['size']); + for ($i=0; $i < $words; ++$i) { + $val = (ord($inputitem['data'][$i*3 ]) - ord('0')) * 100; + $val += (ord($inputitem['data'][$i*3+1]) - ord('0')) * 10; + $val += (ord($inputitem['data'][$i*3+2]) - ord('0')); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 10, $val); + } + if ($inputitem['size'] - $words * 3 == 1) { + $val = ord($inputitem['data'][$words*3]) - ord('0'); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, $val); + } elseif (($inputitem['size'] - ($words * 3)) == 2) { + $val = (ord($inputitem['data'][$words*3 ]) - ord('0')) * 10; + $val += (ord($inputitem['data'][$words*3+1]) - ord('0')); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 7, $val); + } + return $inputitem; + } + + /** + * encodeModeAn + * @param array $inputitem + * @param int $version + * @return array input item + */ + protected function encodeModeAn($inputitem, $version) { + $words = (int)($inputitem['size'] / 2); + $inputitem['bstream'] = array(); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, 0x02); + $inputitem['bstream'] = $this->appendNum(v, $this->lengthIndicator(QR_MODE_AN, $version), $inputitem['size']); + for ($i=0; $i < $words; ++$i) { + $val = (int)$this->lookAnTable(ord($inputitem['data'][$i*2 ])) * 45; + $val += (int)$this->lookAnTable(ord($inputitem['data'][$i*2+1])); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 11, $val); + } + if ($inputitem['size'] & 1) { + $val = $this->lookAnTable(ord($inputitem['data'][($words * 2)])); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 6, $val); + } + return $inputitem; + } + + /** + * encodeMode8 + * @param array $inputitem + * @param int $version + * @return array input item + */ + protected function encodeMode8($inputitem, $version) { + $inputitem['bstream'] = array(); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, 0x4); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], $this->lengthIndicator(QR_MODE_8B, $version), $inputitem['size']); + for ($i=0; $i < $inputitem['size']; ++$i) { + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 8, ord($inputitem['data'][$i])); + } + return $inputitem; + } + + /** + * encodeModeKanji + * @param array $inputitem + * @param int $version + * @return array input item + */ + protected function encodeModeKanji($inputitem, $version) { + $inputitem['bstream'] = array(); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, 0x8); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], $this->lengthIndicator(QR_MODE_KJ, $version), (int)($inputitem['size'] / 2)); + for ($i=0; $i<$inputitem['size']; $i+=2) { + $val = (ord($inputitem['data'][$i]) << 8) | ord($inputitem['data'][$i+1]); + if ($val <= 0x9ffc) { + $val -= 0x8140; + } else { + $val -= 0xc140; + } + $h = ($val >> 8) * 0xc0; + $val = ($val & 0xff) + $h; + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 13, $val); + } + return $inputitem; + } + + /** + * encodeModeStructure + * @param array $inputitem + * @return array input item + */ + protected function encodeModeStructure($inputitem) { + $inputitem['bstream'] = array(); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, 0x03); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, ord($inputitem['data'][1]) - 1); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 4, ord($inputitem['data'][0]) - 1); + $inputitem['bstream'] = $this->appendNum($inputitem['bstream'], 8, ord($inputitem['data'][2])); + return $inputitem; + } + + /** + * encodeBitStream + * @param array $inputitem + * @param int $version + * @return array input item + */ + protected function encodeBitStream($inputitem, $version) { + $inputitem['bstream'] = array(); + $words = $this->maximumWords($inputitem['mode'], $version); + if ($inputitem['size'] > $words) { + $st1 = $this->newInputItem($inputitem['mode'], $words, $inputitem['data']); + $st2 = $this->newInputItem($inputitem['mode'], $inputitem['size'] - $words, array_slice($inputitem['data'], $words)); + $st1 = $this->encodeBitStream($st1, $version); + $st2 = $this->encodeBitStream($st2, $version); + $inputitem['bstream'] = array(); + $inputitem['bstream'] = $this->appendBitstream($inputitem['bstream'], $st1['bstream']); + $inputitem['bstream'] = $this->appendBitstream($inputitem['bstream'], $st2['bstream']); + } else { + switch($inputitem['mode']) { + case QR_MODE_NM: { + $inputitem = $this->encodeModeNum($inputitem, $version); + break; + } + case QR_MODE_AN: { + $inputitem = $this->encodeModeAn($inputitem, $version); + break; + } + case QR_MODE_8B: { + $inputitem = $this->encodeMode8($inputitem, $version); + break; + } + case QR_MODE_KJ: { + $inputitem = $this->encodeModeKanji($inputitem, $version); + break; + } + case QR_MODE_ST: { + $inputitem = $this->encodeModeStructure($inputitem); + break; + } + default: { + break; + } + } + } + return $inputitem; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRinput + + /** + * Append data to an input object. + * The data is copied and appended to the input object. + * @param array items input items + * @param int $mode encoding mode. + * @param int $size size of data (byte). + * @param array $data array of input data. + * @return items + * + */ + protected function appendNewInputItem($items, $mode, $size, $data) { + $items[] = $this->newInputItem($mode, $size, $data); + return $items; + } + + /** + * insertStructuredAppendHeader + * @param array $items + * @param int $size + * @param int $index + * @param int $parity + * @return array items + */ + protected function insertStructuredAppendHeader($items, $size, $index, $parity) { + if ($size > MAX_STRUCTURED_SYMBOLS) { + return -1; + } + if (($index <= 0) OR ($index > MAX_STRUCTURED_SYMBOLS)) { + return -1; + } + $buf = array($size, $index, $parity); + $entry = $this->newInputItem(QR_MODE_ST, 3, buf); + array_unshift($items, $entry); + return $items; + } + + /** + * calcParity + * @param array $items + * @return int parity + */ + protected function calcParity($items) { + $parity = 0; + foreach ($items as $item) { + if ($item['mode'] != QR_MODE_ST) { + for ($i=$item['size']-1; $i>=0; --$i) { + $parity ^= $item['data'][$i]; + } + } + } + return $parity; + } + + /** + * checkModeNum + * @param int $size + * @param array $data + * @return boolean true or false + */ + protected function checkModeNum($size, $data) { + for ($i=0; $i<$size; ++$i) { + if ((ord($data[$i]) < ord('0')) OR (ord($data[$i]) > ord('9'))){ + return false; + } + } + return true; + } + + /** + * estimateBitsModeNum + * @param int $size + * @return int number of bits + */ + protected function estimateBitsModeNum($size) { + $w = (int)$size / 3; + $bits = $w * 10; + switch($size - $w * 3) { + case 1: { + $bits += 4; + break; + } + case 2: { + $bits += 7; + break; + } + default: { + break; + } + } + return $bits; + } + + /** + * Look up the alphabet-numeric convesion table (see JIS X0510:2004, pp.19). + * @param int $c character value + * @return value + */ + protected function lookAnTable($c) { + return (($c > 127)?-1:$this->anTable[$c]); + } + + /** + * checkModeAn + * @param int $size + * @param array $data + * @return boolean true or false + */ + protected function checkModeAn($size, $data) { + for ($i=0; $i<$size; ++$i) { + if ($this->lookAnTable(ord($data[$i])) == -1) { + return false; + } + } + return true; + } + + /** + * estimateBitsModeAn + * @param int $size + * @return int number of bits + */ + protected function estimateBitsModeAn($size) { + $w = (int)($size / 2); + $bits = $w * 11; + if ($size & 1) { + $bits += 6; + } + return $bits; + } + + /** + * estimateBitsMode8 + * @param int $size + * @return int number of bits + */ + protected function estimateBitsMode8($size) { + return $size * 8; + } + + /** + * estimateBitsModeKanji + * @param int $size + * @return int number of bits + */ + protected function estimateBitsModeKanji($size) { + return (int)(($size / 2) * 13); + } + + /** + * checkModeKanji + * @param int $size + * @param array $data + * @return boolean true or false + */ + protected function checkModeKanji($size, $data) { + if ($size & 1) { + return false; + } + for ($i=0; $i<$size; $i+=2) { + $val = (ord($data[$i]) << 8) | ord($data[$i+1]); + if (($val < 0x8140) OR (($val > 0x9ffc) AND ($val < 0xe040)) OR ($val > 0xebbf)) { + return false; + } + } + return true; + } + + /** + * Validate the input data. + * @param int $mode encoding mode. + * @param int $size size of data (byte). + * @param array data data to validate + * @return boolean true in case of valid data, false otherwise + */ + protected function check($mode, $size, $data) { + if ($size <= 0) { + return false; + } + switch($mode) { + case QR_MODE_NM: { + return $this->checkModeNum($size, $data); + } + case QR_MODE_AN: { + return $this->checkModeAn($size, $data); + } + case QR_MODE_KJ: { + return $this->checkModeKanji($size, $data); + } + case QR_MODE_8B: { + return true; + } + case QR_MODE_ST: { + return true; + } + default: { + break; + } + } + return false; + } + + /** + * estimateBitStreamSize + * @param array $items + * @param int $version + * @return int bits + */ + protected function estimateBitStreamSize($items, $version) { + $bits = 0; + if ($version == 0) { + $version = 1; + } + foreach ($items as $item) { + switch($item['mode']) { + case QR_MODE_NM: { + $bits = $this->estimateBitsModeNum($item['size']); + break; + } + case QR_MODE_AN: { + $bits = $this->estimateBitsModeAn($item['size']); + break; + } + case QR_MODE_8B: { + $bits = $this->estimateBitsMode8($item['size']); + break; + } + case QR_MODE_KJ: { + $bits = $this->estimateBitsModeKanji($item['size']); + break; + } + case QR_MODE_ST: { + return STRUCTURE_HEADER_BITS; + } + default: { + return 0; + } + } + $l = $this->lengthIndicator($item['mode'], $version); + $m = 1 << $l; + $num = (int)(($item['size'] + $m - 1) / $m); + $bits += $num * (4 + $l); + } + return $bits; + } + + /** + * estimateVersion + * @param array $items + * @return int version + */ + protected function estimateVersion($items) { + $version = 0; + $prev = 0; + do { + $prev = $version; + $bits = $this->estimateBitStreamSize($items, $prev); + $version = $this->getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if ($version < 0) { + return -1; + } + } while ($version > $prev); + return $version; + } + + /** + * lengthOfCode + * @param int $mode + * @param int $version + * @param int $bits + * @return int size + */ + protected function lengthOfCode($mode, $version, $bits) { + $payload = $bits - 4 - $this->lengthIndicator($mode, $version); + switch($mode) { + case QR_MODE_NM: { + $chunks = (int)($payload / 10); + $remain = $payload - $chunks * 10; + $size = $chunks * 3; + if ($remain >= 7) { + $size += 2; + } elseif ($remain >= 4) { + $size += 1; + } + break; + } + case QR_MODE_AN: { + $chunks = (int)($payload / 11); + $remain = $payload - $chunks * 11; + $size = $chunks * 2; + if ($remain >= 6) { + ++$size; + } + break; + } + case QR_MODE_8B: { + $size = (int)($payload / 8); + break; + } + case QR_MODE_KJ: { + $size = (int)(($payload / 13) * 2); + break; + } + case QR_MODE_ST: { + $size = (int)($payload / 8); + break; + } + default: { + $size = 0; + break; + } + } + $maxsize = $this->maximumWords($mode, $version); + if ($size < 0) { + $size = 0; + } + if ($size > $maxsize) { + $size = $maxsize; + } + return $size; + } + + /** + * createBitStream + * @param array $items + * @return array of items and total bits + */ + protected function createBitStream($items) { + $total = 0; + foreach ($items as $key => $item) { + $items[$key] = $this->encodeBitStream($item, $this->version); + $bits = count($items[$key]['bstream']); + $total += $bits; + } + return array($items, $total); + } + + /** + * convertData + * @param array $items + * @return array items + */ + protected function convertData($items) { + $ver = $this->estimateVersion($items); + if ($ver > $this->version) { + $this->version = $ver; + } + for (;;) { + $cbs = $this->createBitStream($items); + $items = $cbs[0]; + $bits = $cbs[1]; + if ($bits < 0) { + return -1; + } + $ver = $this->getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if ($ver < 0) { + return -1; + } elseif ($ver > $this->version) { + $this->version = $ver; + } else { + break; + } + } + return $items; + } + + /** + * Append Padding Bit to bitstream + * @param array $bstream + * @return array bitstream + */ + protected function appendPaddingBit($bstream) { + $bits = count($bstream); + $maxwords = $this->getDataLength($this->version, $this->level); + $maxbits = $maxwords * 8; + if ($maxbits == $bits) { + return 0; + } + if ($maxbits - $bits < 5) { + return $this->appendNum($bstream, $maxbits - $bits, 0); + } + $bits += 4; + $words = (int)(($bits + 7) / 8); + $padding = array(); + $padding = $this->appendNum($padding, $words * 8 - $bits + 4, 0); + $padlen = $maxwords - $words; + if ($padlen > 0) { + $padbuf = array(); + for ($i=0; $i<$padlen; ++$i) { + $padbuf[$i] = ($i&1)?0x11:0xec; + } + $padding = $this->appendBytes($padding, $padlen, $padbuf); + } + return $this->appendBitstream($bstream, $padding); + } + + /** + * mergeBitStream + * @param array $bstream + * @return array bitstream + */ + protected function mergeBitStream($items) { + $items = $this->convertData($items); + $bstream = array(); + foreach ($items as $item) { + $bstream = $this->appendBitstream($bstream, $item['bstream']); + } + return $bstream; + } + + /** + * Returns a stream of bits. + * @param int $items + * @return array padded merged byte stream + */ + protected function getBitStream($items) { + $bstream = $this->mergeBitStream($items); + return $this->appendPaddingBit($bstream); + } + + /** + * Pack all bit streams padding bits into a byte array. + * @param int $items + * @return array padded merged byte stream + */ + protected function getByteStream($items) { + $bstream = $this->getBitStream($items); + return $this->bitstreamToByte($bstream); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRbitstream + + /** + * Return an array with zeros + * @param int $setLength array size + * @return array + */ + protected function allocate($setLength) { + return array_fill(0, $setLength, 0); + } + + /** + * Return new bitstream from number + * @param int $bits number of bits + * @param int $num number + * @return array bitstream + */ + protected function newFromNum($bits, $num) { + $bstream = $this->allocate($bits); + $mask = 1 << ($bits - 1); + for ($i=0; $i<$bits; ++$i) { + if ($num & $mask) { + $bstream[$i] = 1; + } else { + $bstream[$i] = 0; + } + $mask = $mask >> 1; + } + return $bstream; + } + + /** + * Return new bitstream from bytes + * @param int $size size + * @param array $data bytes + * @return array bitstream + */ + protected function newFromBytes($size, $data) { + $bstream = $this->allocate($size * 8); + $p=0; + for ($i=0; $i<$size; ++$i) { + $mask = 0x80; + for ($j=0; $j<8; ++$j) { + if ($data[$i] & $mask) { + $bstream[$p] = 1; + } else { + $bstream[$p] = 0; + } + $p++; + $mask = $mask >> 1; + } + } + return $bstream; + } + + /** + * Append one bitstream to another + * @param array $bitstream original bitstream + * @param array $append bitstream to append + * @return array bitstream + */ + protected function appendBitstream($bitstream, $append) { + if ((!is_array($append)) OR (count($append) == 0)) { + return $bitstream; + } + if (count($bitstream) == 0) { + return $append; + } + return array_values(array_merge($bitstream, $append)); + } + + /** + * Append one bitstream created from number to another + * @param array $bitstream original bitstream + * @param int $bits number of bits + * @param int $num number + * @return array bitstream + */ + protected function appendNum($bitstream, $bits, $num) { + if ($bits == 0) { + return 0; + } + $b = $this->newFromNum($bits, $num); + return $this->appendBitstream($bitstream, $b); + } + + /** + * Append one bitstream created from bytes to another + * @param array $bitstream original bitstream + * @param int $size size + * @param array $data bytes + * @return array bitstream + */ + protected function appendBytes($bitstream, $size, $data) { + if ($size == 0) { + return 0; + } + $b = $this->newFromBytes($size, $data); + return $this->appendBitstream($bitstream, $b); + } + + /** + * Convert bitstream to bytes + * @param array $bitstream original bitstream + * @return array of bytes + */ + protected function bitstreamToByte($bstream) { + $size = count($bstream); + if ($size == 0) { + return array(); + } + $data = array_fill(0, (int)(($size + 7) / 8), 0); + $bytes = (int)($size / 8); + $p = 0; + for ($i=0; $i<$bytes; $i++) { + $v = 0; + for ($j=0; $j<8; $j++) { + $v = $v << 1; + $v |= $bstream[$p]; + $p++; + } + $data[$i] = $v; + } + if ($size & 7) { + $v = 0; + for ($j=0; $j<($size & 7); $j++) { + $v = $v << 1; + $v |= $bstream[$p]; + $p++; + } + $data[$bytes] = $v; + } + return $data; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRspec + + /** + * Replace a value on the array at the specified position + * @param array $srctab + * @param int $x X position + * @param int $y Y position + * @param string $repl value to replace + * @param int $replLen length of the repl string + * @return array srctab + */ + protected function qrstrset($srctab, $x, $y, $repl, $replLen=false) { + $srctab[$y] = substr_replace($srctab[$y], ($replLen !== false)?substr($repl,0,$replLen):$repl, $x, ($replLen !== false)?$replLen:strlen($repl)); + return $srctab; + } + + /** + * Return maximum data code length (bytes) for the version. + * @param int $version version + * @param int $level error correction level + * @return int maximum size (bytes) + */ + protected function getDataLength($version, $level) { + return $this->capacity[$version][QRCAP_WORDS] - $this->capacity[$version][QRCAP_EC][$level]; + } + + /** + * Return maximum error correction code length (bytes) for the version. + * @param int $version version + * @param int $level error correction level + * @return int ECC size (bytes) + */ + protected function getECCLength($version, $level){ + return $this->capacity[$version][QRCAP_EC][$level]; + } + + /** + * Return the width of the symbol for the version. + * @param int $version version + * @return int width + */ + protected function getWidth($version) { + return $this->capacity[$version][QRCAP_WIDTH]; + } + + /** + * Return the numer of remainder bits. + * @param int $version version + * @return int number of remainder bits + */ + protected function getRemainder($version) { + return $this->capacity[$version][QRCAP_REMINDER]; + } + + /** + * Return a version number that satisfies the input code length. + * @param int $size input code length (byte) + * @param int $level error correction level + * @return int version number + */ + protected function getMinimumVersion($size, $level) { + for ($i=1; $i <= QRSPEC_VERSION_MAX; ++$i) { + $words = $this->capacity[$i][QRCAP_WORDS] - $this->capacity[$i][QRCAP_EC][$level]; + if ($words >= $size) { + return $i; + } + } + return -1; + } + + /** + * Return the size of length indicator for the mode and version. + * @param int $mode encoding mode + * @param int $version version + * @return int the size of the appropriate length indicator (bits). + */ + protected function lengthIndicator($mode, $version) { + if ($mode == QR_MODE_ST) { + return 0; + } + if ($version <= 9) { + $l = 0; + } elseif ($version <= 26) { + $l = 1; + } else { + $l = 2; + } + return $this->lengthTableBits[$mode][$l]; + } + + /** + * Return the maximum length for the mode and version. + * @param int $mode encoding mode + * @param int $version version + * @return int the maximum length (bytes) + */ + protected function maximumWords($mode, $version) { + if ($mode == QR_MODE_ST) { + return 3; + } + if ($version <= 9) { + $l = 0; + } else if ($version <= 26) { + $l = 1; + } else { + $l = 2; + } + $bits = $this->lengthTableBits[$mode][$l]; + $words = (1 << $bits) - 1; + if ($mode == QR_MODE_KJ) { + $words *= 2; // the number of bytes is required + } + return $words; + } + + /** + * Return an array of ECC specification. + * @param int $version version + * @param int $level error correction level + * @param array $spec an array of ECC specification contains as following: {# of type1 blocks, # of data code, # of ecc code, # of type2 blocks, # of data code} + * @return array spec + */ + protected function getEccSpec($version, $level, $spec) { + if (count($spec) < 5) { + $spec = array(0, 0, 0, 0, 0); + } + $b1 = $this->eccTable[$version][$level][0]; + $b2 = $this->eccTable[$version][$level][1]; + $data = $this->getDataLength($version, $level); + $ecc = $this->getECCLength($version, $level); + if ($b2 == 0) { + $spec[0] = $b1; + $spec[1] = (int)($data / $b1); + $spec[2] = (int)($ecc / $b1); + $spec[3] = 0; + $spec[4] = 0; + } else { + $spec[0] = $b1; + $spec[1] = (int)($data / ($b1 + $b2)); + $spec[2] = (int)($ecc / ($b1 + $b2)); + $spec[3] = $b2; + $spec[4] = $spec[1] + 1; + } + return $spec; + } + + /** + * Put an alignment marker. + * @param array $frame frame + * @param int $width width + * @param int $ox X center coordinate of the pattern + * @param int $oy Y center coordinate of the pattern + * @return array frame + */ + protected function putAlignmentMarker($frame, $ox, $oy) { + $finder = array( + "\xa1\xa1\xa1\xa1\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa0\xa1\xa0\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa1\xa1\xa1\xa1" + ); + $yStart = $oy - 2; + $xStart = $ox - 2; + for ($y=0; $y < 5; $y++) { + $frame = $this->qrstrset($frame, $xStart, $yStart+$y, $finder[$y]); + } + return $frame; + } + + /** + * Put an alignment pattern. + * @param int $version version + * @param array $fram frame + * @param int $width width + * @return array frame + */ + protected function putAlignmentPattern($version, $frame, $width) { + if ($version < 2) { + return $frame; + } + $d = $this->alignmentPattern[$version][1] - $this->alignmentPattern[$version][0]; + if ($d < 0) { + $w = 2; + } else { + $w = (int)(($width - $this->alignmentPattern[$version][0]) / $d + 2); + } + if ($w * $w - 3 == 1) { + $x = $this->alignmentPattern[$version][0]; + $y = $this->alignmentPattern[$version][0]; + $frame = $this->putAlignmentMarker($frame, $x, $y); + return $frame; + } + $cx = $this->alignmentPattern[$version][0]; + $wo = $w - 1; + for ($x=1; $x < $wo; ++$x) { + $frame = $this->putAlignmentMarker($frame, 6, $cx); + $frame = $this->putAlignmentMarker($frame, $cx, 6); + $cx += $d; + } + $cy = $this->alignmentPattern[$version][0]; + for ($y=0; $y < $wo; ++$y) { + $cx = $this->alignmentPattern[$version][0]; + for ($x=0; $x < $wo; ++$x) { + $frame = $this->putAlignmentMarker($frame, $cx, $cy); + $cx += $d; + } + $cy += $d; + } + return $frame; + } + + /** + * Return BCH encoded version information pattern that is used for the symbol of version 7 or greater. Use lower 18 bits. + * @param int $version version + * @return BCH encoded version information pattern + */ + protected function getVersionPattern($version) { + if (($version < 7) OR ($version > QRSPEC_VERSION_MAX)) { + return 0; + } + return $this->versionPattern[($version - 7)]; + } + + /** + * Return BCH encoded format information pattern. + * @param array $mask + * @param int $level error correction level + * @return BCH encoded format information pattern + */ + protected function getFormatInfo($mask, $level) { + if (($mask < 0) OR ($mask > 7)) { + return 0; + } + if (($level < 0) OR ($level > 3)) { + return 0; + } + return $this->formatInfo[$level][$mask]; + } + + /** + * Put a finder pattern. + * @param array $frame frame + * @param int $width width + * @param int $ox X center coordinate of the pattern + * @param int $oy Y center coordinate of the pattern + * @return array frame + */ + protected function putFinderPattern($frame, $ox, $oy) { + $finder = array( + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1" + ); + for ($y=0; $y < 7; $y++) { + $frame = $this->qrstrset($frame, $ox, ($oy + $y), $finder[$y]); + } + return $frame; + } + + /** + * Return a copy of initialized frame. + * @param int $version version + * @return Array of unsigned char. + */ + protected function createFrame($version) { + $width = $this->capacity[$version][QRCAP_WIDTH]; + $frameLine = str_repeat ("\0", $width); + $frame = array_fill(0, $width, $frameLine); + // Finder pattern + $frame = $this->putFinderPattern($frame, 0, 0); + $frame = $this->putFinderPattern($frame, $width - 7, 0); + $frame = $this->putFinderPattern($frame, 0, $width - 7); + // Separator + $yOffset = $width - 7; + for ($y=0; $y < 7; ++$y) { + $frame[$y][7] = "\xc0"; + $frame[$y][$width - 8] = "\xc0"; + $frame[$yOffset][7] = "\xc0"; + ++$yOffset; + } + $setPattern = str_repeat("\xc0", 8); + $frame = $this->qrstrset($frame, 0, 7, $setPattern); + $frame = $this->qrstrset($frame, $width-8, 7, $setPattern); + $frame = $this->qrstrset($frame, 0, $width - 8, $setPattern); + // Format info + $setPattern = str_repeat("\x84", 9); + $frame = $this->qrstrset($frame, 0, 8, $setPattern); + $frame = $this->qrstrset($frame, $width - 8, 8, $setPattern, 8); + $yOffset = $width - 8; + for ($y=0; $y < 8; ++$y,++$yOffset) { + $frame[$y][8] = "\x84"; + $frame[$yOffset][8] = "\x84"; + } + // Timing pattern + $wo = $width - 15; + for ($i=1; $i < $wo; ++$i) { + $frame[6][7+$i] = chr(0x90 | ($i & 1)); + $frame[7+$i][6] = chr(0x90 | ($i & 1)); + } + // Alignment pattern + $frame = $this->putAlignmentPattern($version, $frame, $width); + // Version information + if ($version >= 7) { + $vinf = $this->getVersionPattern($version); + $v = $vinf; + for ($x=0; $x<6; ++$x) { + for ($y=0; $y<3; ++$y) { + $frame[($width - 11)+$y][$x] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + $v = $vinf; + for ($y=0; $y<6; ++$y) { + for ($x=0; $x<3; ++$x) { + $frame[$y][$x+($width - 11)] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + } + // and a little bit... + $frame[$width - 8][8] = "\x81"; + return $frame; + } + + /** + * Set new frame for the specified version. + * @param int $version version + * @return Array of unsigned char. + */ + protected function newFrame($version) { + if (($version < 1) OR ($version > QRSPEC_VERSION_MAX)) { + return NULL; + } + if (!isset($this->frames[$version])) { + $this->frames[$version] = $this->createFrame($version); + } + if (is_null($this->frames[$version])) { + return NULL; + } + return $this->frames[$version]; + } + + /** + * Return block number 0 + * @param array $spec + * @return int value + */ + protected function rsBlockNum($spec) { + return ($spec[0] + $spec[3]); + } + + /** + * Return block number 1 + * @param array $spec + * @return int value + */ + protected function rsBlockNum1($spec) { + return $spec[0]; + } + + /** + * Return data codes 1 + * @param array $spec + * @return int value + */ + protected function rsDataCodes1($spec) { + return $spec[1]; + } + + /** + * Return ecc codes 1 + * @param array $spec + * @return int value + */ + protected function rsEccCodes1($spec) { + return $spec[2]; + } + + /** + * Return block number 2 + * @param array $spec + * @return int value + */ + protected function rsBlockNum2($spec) { + return $spec[3]; + } + + /** + * Return data codes 2 + * @param array $spec + * @return int value + */ + protected function rsDataCodes2($spec) { + return $spec[4]; + } + + /** + * Return ecc codes 2 + * @param array $spec + * @return int value + */ + protected function rsEccCodes2($spec) { + return $spec[2]; + } + + /** + * Return data length + * @param array $spec + * @return int value + */ + protected function rsDataLength($spec) { + return ($spec[0] * $spec[1]) + ($spec[3] * $spec[4]); + } + + /** + * Return ecc length + * @param array $spec + * @return int value + */ + protected function rsEccLength($spec) { + return ($spec[0] + $spec[3]) * $spec[2]; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRrs + + /** + * Initialize a Reed-Solomon codec and add it to existing rsitems + * @param int $symsize symbol size, bits + * @param int $gfpoly Field generator polynomial coefficients + * @param int $fcr first root of RS code generator polynomial, index form + * @param int $prim primitive element to generate polynomial roots + * @param int $nroots RS code generator polynomial degree (number of roots) + * @param int $pad padding bytes at front of shortened block + * @return array Array of RS values:
  • mm = Bits per symbol;
  • nn = Symbols per block;
  • alpha_to = log lookup table array;
  • index_of = Antilog lookup table array;
  • genpoly = Generator polynomial array;
  • nroots = Number of generator;
  • roots = number of parity symbols;
  • fcr = First consecutive root, index form;
  • prim = Primitive element, index form;
  • iprim = prim-th root of 1, index form;
  • pad = Padding bytes in shortened block;
  • gfpoly
. + */ + protected function init_rs($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) { + foreach ($this->rsitems as $rs) { + if (($rs['pad'] != $pad) OR ($rs['nroots'] != $nroots) OR ($rs['mm'] != $symsize) + OR ($rs['gfpoly'] != $gfpoly) OR ($rs['fcr'] != $fcr) OR ($rs['prim'] != $prim)) { + continue; + } + return $rs; + } + $rs = $this->init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad); + array_unshift($this->rsitems, $rs); + return $rs; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - + + // QRrsItem + + /** + * modnn + * @param array RS values + * @param int $x X position + * @return int X osition + */ + protected function modnn($rs, $x) { + while ($x >= $rs['nn']) { + $x -= $rs['nn']; + $x = ($x >> $rs['mm']) + ($x & $rs['nn']); + } + return $x; + } + + /** + * Initialize a Reed-Solomon codec and returns an array of values. + * @param int $symsize symbol size, bits + * @param int $gfpoly Field generator polynomial coefficients + * @param int $fcr first root of RS code generator polynomial, index form + * @param int $prim primitive element to generate polynomial roots + * @param int $nroots RS code generator polynomial degree (number of roots) + * @param int $pad padding bytes at front of shortened block + * @return array Array of RS values:
  • mm = Bits per symbol;
  • nn = Symbols per block;
  • alpha_to = log lookup table array;
  • index_of = Antilog lookup table array;
  • genpoly = Generator polynomial array;
  • nroots = Number of generator;
  • roots = number of parity symbols;
  • fcr = First consecutive root, index form;
  • prim = Primitive element, index form;
  • iprim = prim-th root of 1, index form;
  • pad = Padding bytes in shortened block;
  • gfpoly
. + */ + protected function init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) { + // Based on Reed solomon encoder by Phil Karn, KA9Q (GNU-LGPLv2) + $rs = null; + // Check parameter ranges + if (($symsize < 0) OR ($symsize > 8)) { + return $rs; + } + if (($fcr < 0) OR ($fcr >= (1<<$symsize))) { + return $rs; + } + if (($prim <= 0) OR ($prim >= (1<<$symsize))) { + return $rs; + } + if (($nroots < 0) OR ($nroots >= (1<<$symsize))) { + return $rs; + } + if (($pad < 0) OR ($pad >= ((1<<$symsize) -1 - $nroots))) { + return $rs; + } + $rs = array(); + $rs['mm'] = $symsize; + $rs['nn'] = (1 << $symsize) - 1; + $rs['pad'] = $pad; + $rs['alpha_to'] = array_fill(0, ($rs['nn'] + 1), 0); + $rs['index_of'] = array_fill(0, ($rs['nn'] + 1), 0); + // PHP style macro replacement ;) + $NN =& $rs['nn']; + $A0 =& $NN; + // Generate Galois field lookup tables + $rs['index_of'][0] = $A0; // log(zero) = -inf + $rs['alpha_to'][$A0] = 0; // alpha**-inf = 0 + $sr = 1; + for ($i=0; $i<$rs['nn']; ++$i) { + $rs['index_of'][$sr] = $i; + $rs['alpha_to'][$i] = $sr; + $sr <<= 1; + if ($sr & (1 << $symsize)) { + $sr ^= $gfpoly; + } + $sr &= $rs['nn']; + } + if ($sr != 1) { + // field generator polynomial is not primitive! + return NULL; + } + // Form RS code generator polynomial from its roots + $rs['genpoly'] = array_fill(0, ($nroots + 1), 0); + $rs['fcr'] = $fcr; + $rs['prim'] = $prim; + $rs['nroots'] = $nroots; + $rs['gfpoly'] = $gfpoly; + // Find prim-th root of 1, used in decoding + for ($iprim=1; ($iprim % $prim) != 0; $iprim += $rs['nn']) { + ; // intentional empty-body loop! + } + $rs['iprim'] = (int)($iprim / $prim); + $rs['genpoly'][0] = 1; + + + for ($i = 0,$root=$fcr*$prim; $i < $nroots; $i++, $root += $prim) { + $rs['genpoly'][$i+1] = 1; + // Multiply rs->genpoly[] by @**(root + x) + for ($j = $i; $j > 0; --$j) { + if ($rs['genpoly'][$j] != 0) { + $rs['genpoly'][$j] = $rs['genpoly'][$j-1] ^ $rs['alpha_to'][$this->modnn($rs, $rs['index_of'][$rs['genpoly'][$j]] + $root)]; + } else { + $rs['genpoly'][$j] = $rs['genpoly'][$j-1]; + } + } + // rs->genpoly[0] can never be zero + $rs['genpoly'][0] = $rs['alpha_to'][$this->modnn($rs, $rs['index_of'][$rs['genpoly'][0]] + $root)]; + } + // convert rs->genpoly[] to index form for quicker encoding + for ($i = 0; $i <= $nroots; ++$i) { + $rs['genpoly'][$i] = $rs['index_of'][$rs['genpoly'][$i]]; + } + return $rs; + } + + /** + * Encode a Reed-Solomon codec and returns the parity array + * @param array $rs RS values + * @param array $data data + * @param array $parity parity + * @return parity array + */ + protected function encode_rs_char($rs, $data, $parity) { + $MM =& $rs['mm']; // bits per symbol + $NN =& $rs['nn']; // the total number of symbols in a RS block + $ALPHA_TO =& $rs['alpha_to']; // the address of an array of NN elements to convert Galois field elements in index (log) form to polynomial form + $INDEX_OF =& $rs['index_of']; // the address of an array of NN elements to convert Galois field elements in polynomial form to index (log) form + $GENPOLY =& $rs['genpoly']; // an array of NROOTS+1 elements containing the generator polynomial in index form + $NROOTS =& $rs['nroots']; // the number of roots in the RS code generator polynomial, which is the same as the number of parity symbols in a block + $FCR =& $rs['fcr']; // first consecutive root, index form + $PRIM =& $rs['prim']; // primitive element, index form + $IPRIM =& $rs['iprim']; // prim-th root of 1, index form + $PAD =& $rs['pad']; // the number of pad symbols in a block + $A0 =& $NN; + $parity = array_fill(0, $NROOTS, 0); + for ($i=0; $i < ($NN - $NROOTS - $PAD); $i++) { + $feedback = $INDEX_OF[$data[$i] ^ $parity[0]]; + if ($feedback != $A0) { + // feedback term is non-zero + // This line is unnecessary when GENPOLY[NROOTS] is unity, as it must + // always be for the polynomials constructed by init_rs() + $feedback = $this->modnn($rs, $NN - $GENPOLY[$NROOTS] + $feedback); + for ($j=1; $j < $NROOTS; ++$j) { + $parity[$j] ^= $ALPHA_TO[$this->modnn($rs, $feedback + $GENPOLY[($NROOTS - $j)])]; + } + } + // Shift + array_shift($parity); + if ($feedback != $A0) { + array_push($parity, $ALPHA_TO[$this->modnn($rs, $feedback + $GENPOLY[0])]); + } else { + array_push($parity, 0); + } + } + return $parity; + } + + } // end QRcode class + +} // END OF "class_exists QRcode" +?> diff --git a/application/libraries/phpqrcode/cache/frame_1.dat b/application/libraries/phpqrcode/cache/frame_1.dat new file mode 100644 index 0000000..be28fea --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_1.dat @@ -0,0 +1,2 @@ +xڝ�Á À E9³u��`³"PńCÛ牗T!0$ +E•ɲQ™�Ém½úh۾9{kI"› 9Ln)Ap¤å־Ë>ß^‡Õz³mënŖ;ü´mßn†ú¦Ë \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_1.png b/application/libraries/phpqrcode/cache/frame_1.png new file mode 100644 index 0000000..86ce6e9 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_1.png differ diff --git a/application/libraries/phpqrcode/cache/frame_10.dat b/application/libraries/phpqrcode/cache/frame_10.dat new file mode 100644 index 0000000..aff163f Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_10.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_10.png b/application/libraries/phpqrcode/cache/frame_10.png new file mode 100644 index 0000000..dbfcd70 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_10.png differ diff --git a/application/libraries/phpqrcode/cache/frame_11.dat b/application/libraries/phpqrcode/cache/frame_11.dat new file mode 100644 index 0000000..95af68a Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_11.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_11.png b/application/libraries/phpqrcode/cache/frame_11.png new file mode 100644 index 0000000..c07c761 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_11.png differ diff --git a/application/libraries/phpqrcode/cache/frame_12.dat b/application/libraries/phpqrcode/cache/frame_12.dat new file mode 100644 index 0000000..73228b3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_12.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_12.png b/application/libraries/phpqrcode/cache/frame_12.png new file mode 100644 index 0000000..8ba6782 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_12.png differ diff --git a/application/libraries/phpqrcode/cache/frame_13.dat b/application/libraries/phpqrcode/cache/frame_13.dat new file mode 100644 index 0000000..2256f0e Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_13.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_13.png b/application/libraries/phpqrcode/cache/frame_13.png new file mode 100644 index 0000000..6e49d35 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_13.png differ diff --git a/application/libraries/phpqrcode/cache/frame_14.dat b/application/libraries/phpqrcode/cache/frame_14.dat new file mode 100644 index 0000000..e9ae093 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_14.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_14.png b/application/libraries/phpqrcode/cache/frame_14.png new file mode 100644 index 0000000..efc36c0 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_14.png differ diff --git a/application/libraries/phpqrcode/cache/frame_15.dat b/application/libraries/phpqrcode/cache/frame_15.dat new file mode 100644 index 0000000..1872781 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_15.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_15.png b/application/libraries/phpqrcode/cache/frame_15.png new file mode 100644 index 0000000..a9f416c Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_15.png differ diff --git a/application/libraries/phpqrcode/cache/frame_16.dat b/application/libraries/phpqrcode/cache/frame_16.dat new file mode 100644 index 0000000..60af678 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_16.dat @@ -0,0 +1 @@ +xÚí™A„ E]sëIX´;¸Ün6€È`‚q”êêW6ñ奚`Œ%A/3!¢°‚¢Š!g–È̡’1N) éE¢Ï|;®—>6⸏Þ97$ëÄôëc]kkö�wé1Öü[·m­C͜cÊRºÄê¹>¦èµ¾šE,•hʼnp„#áxF�yWÏÇVWGçòÕ3¼Õ+шþà˓úSŽâ}Ğ�#áG8b^c^cÏÀŽp„c&3YQ"ñŽ÷çÌvµù›…ñàÎþþ¼–¹kÞ9Š܇÷}”¹³ï×ú ¢Ŀ�QäÿL—/ÝÔÀÏ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_16.png b/application/libraries/phpqrcode/cache/frame_16.png new file mode 100644 index 0000000..6ac8fe8 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_16.png differ diff --git a/application/libraries/phpqrcode/cache/frame_17.dat b/application/libraries/phpqrcode/cache/frame_17.dat new file mode 100644 index 0000000..87f0cf5 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_17.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_17.png b/application/libraries/phpqrcode/cache/frame_17.png new file mode 100644 index 0000000..5b929ac Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_17.png differ diff --git a/application/libraries/phpqrcode/cache/frame_18.dat b/application/libraries/phpqrcode/cache/frame_18.dat new file mode 100644 index 0000000..bb7138c --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_18.dat @@ -0,0 +1,2 @@ +xÚí™A +ƒ0E]çօ,2;sƒä&É͚h¥ÛêO¡ôÝÈàã1&09OIv@DDÒ Ì&§ىK�XÈÕFv•<Ádqò9Ö<%h•¹ Yïs !(d¥²ës;~||b(ÏøYůg#µ`œK ±S¼Åô¹Ķ˜ùsàidߍLg:әÎtþ/gmª�™ƒkÅMâ3³{­4rTÈQýÿe¥·s·>ó<әÎt¦3�éÌ;ïH¼#љÎt¦3�ÍYœ+og©hù¶óµٽ¬lnðûF>Øi^»#awm;gè~pÛgìNs{6z’‘»ãºïÞäp¾Ê' \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_18.png b/application/libraries/phpqrcode/cache/frame_18.png new file mode 100644 index 0000000..ee0d6a3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_18.png differ diff --git a/application/libraries/phpqrcode/cache/frame_19.dat b/application/libraries/phpqrcode/cache/frame_19.dat new file mode 100644 index 0000000..95e26ad --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_19.dat @@ -0,0 +1,3 @@ +xÚíšA +Ä E»öÖ.ÌNo 7ћ�¶iiRÚN2‹áW%ðxÁ@Úڜê'­ +u�6×êˆ.�*S;}˜«ÒàÏT úèÌzr�t¹ï%ç,ÒÅÚâÎ}ç;“âç)¹Ÿ˜âÝZÚîLåè¹÷¬Pçç$¯×÷ÏqËgœLÂôdJ‡;Üáw¸Ãý.]z#Ÿ¾«[͝½ïOg‚­Æô"ÐË áBíî¦}Ç}‡;Üáw¸Ãî˜#1GbŽ„;Üáw¸Ãý_ÝC+w¢@Dfî÷ïç™u�ø2™ÅÚÉNþû9R7|pWßkïû®¿“ßßkºö¿ºú»¼ÎÓ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_19.png b/application/libraries/phpqrcode/cache/frame_19.png new file mode 100644 index 0000000..20fddd8 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_19.png differ diff --git a/application/libraries/phpqrcode/cache/frame_2.dat b/application/libraries/phpqrcode/cache/frame_2.dat new file mode 100644 index 0000000..7e42f31 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_2.dat @@ -0,0 +1 @@ +xÚ͒Í À F{vë&  à&°Y+?Z1öÐSŸ'y!¢ŸÌÁa�815&£•۴ŽÙHå£ٞc³•l«ÏFÆè1º#é6 fÊÖü©§6Äø•O7ˆ¨†C¦«›ð֏ž�Ï8gI®ÏöfB¦ÃÄÿæ\DԻ( \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_2.png b/application/libraries/phpqrcode/cache/frame_2.png new file mode 100644 index 0000000..9c150eb Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_2.png differ diff --git a/application/libraries/phpqrcode/cache/frame_20.dat b/application/libraries/phpqrcode/cache/frame_20.dat new file mode 100644 index 0000000..d5ecc1d Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_20.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_20.png b/application/libraries/phpqrcode/cache/frame_20.png new file mode 100644 index 0000000..23a061d Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_20.png differ diff --git a/application/libraries/phpqrcode/cache/frame_21.dat b/application/libraries/phpqrcode/cache/frame_21.dat new file mode 100644 index 0000000..1974dd9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_21.dat @@ -0,0 +1 @@ +xÚíšA„ E]sëIX´;¹Ün6Up‚�“в™ÿ]٘þ< i-eWö‹¶˜)×äŕ¼É…H\jvqÙHL\6–šÝЅrI›¢LܹÜÕ%ÅÓ@´þ±V—vÆÂúý¤(ÏP4|ÎXnÒgɝß¼~]D¾ÉÕ×u1Us S\À°€,ÿÅ2Þ¢N§Ã?D›KºüF-:“eJ]p_À°€,˜a0Ã`†ÁÝ XÀ°`†Á ƒw,` X´]˜ˆ™‚¹‹˜°5 ‰®Y4{屿ñ2íûåvçJs†±Ûí9±˜í)õu±۹êÏØ,«]¸“‹Ù^_§7$ƒ_Í \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_21.png b/application/libraries/phpqrcode/cache/frame_21.png new file mode 100644 index 0000000..291598c Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_21.png differ diff --git a/application/libraries/phpqrcode/cache/frame_22.dat b/application/libraries/phpqrcode/cache/frame_22.dat new file mode 100644 index 0000000..0f01802 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_22.dat @@ -0,0 +1,3 @@ +xÚíšA +„0 E]{ë�.’]{{{³©Z¥BepÆÞwe@�V›ERZ3»Á"*2o€4¦y‰)i#dÒbdF҅´ŒI"ú‘—4ž½W­IíuŠÓ45ßx«.Z­SÙ{ÁŸ¯8åËÿk={o.±qÊÙ£[œÍ:帒q»õƒy +)t#á„N8ádCj�-O�OG}¼:/Ÿ:s�z!Å)^<ùe½·S·uâ{ 'œp 'ú=ú=ú=¾'œp 'œp¢ߣߣßã�N8á„Óÿ9©ªˆôpQQõ]HÔpz¾�ØGœ^æ½Qº˜I|¾߳�u;9™ÎïÕëd;“X~$ËÙÑÉt¶ÊÛédy \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_22.png b/application/libraries/phpqrcode/cache/frame_22.png new file mode 100644 index 0000000..bc97bd0 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_22.png differ diff --git a/application/libraries/phpqrcode/cache/frame_23.dat b/application/libraries/phpqrcode/cache/frame_23.dat new file mode 100644 index 0000000..ee3b370 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_23.dat @@ -0,0 +1,3 @@ +xÚíšA +à E³öÖfo 7ћU�) %M!ΔÂûYu(<šð“sK²“Tœ›Ó +É&§IÚ\i+¥Ъ™(m®´FQ¡¹¯h±æöüèv~n1„oÏ]sëçÖï¤_ޟÊ3`î_w2õȹ•lc[¼•;·Ûc֟ˤ’Nóª4Üpà 7ÜpÃímTÿ¸œ›‘ÝêrÞiñä_ƒç¿pS=7Þ7Üpà 7ÜpÍ>IŸ¤Oò-Á 7Üpà 7ú$}’>ɷ7Üpà ·tss‰Órs §åVÍÎÜÆ÷’mýï¡ҹò‡�Þñ}R~7ôà&¾÷º?7ù�Þý�ԦIïbhâ{æ»<ÀMi- \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_23.png b/application/libraries/phpqrcode/cache/frame_23.png new file mode 100644 index 0000000..b8f16ae Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_23.png differ diff --git a/application/libraries/phpqrcode/cache/frame_24.dat b/application/libraries/phpqrcode/cache/frame_24.dat new file mode 100644 index 0000000..7b92e29 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_24.dat @@ -0,0 +1 @@ +xÚí›Aƒ E»öÖMX0;¸�ÜnVP4ÚHSS»xßU3±/O´ý LiJ4�ž±Vâ JCŠ%ý‰6VR&ÃÞD‘BœHjDù‚JÏ??™¯êBl­cDZñ½§'óU­ëXïUïޏ0æÃywÍį÷j¬é똳€3ś¾ë˜cj†ù£{¨¥½:GqÄGÿÝñøŸûÚ°N†v;¹¶笓J ‡ÄÐ<û‡É]ŽêëÈóˆ#Ž8âˆ#Ž8âH'§“ÓÉùÍÁGqÄGéätr:9Ï#Ž8âˆ#Ž8âؓh­ˆ¯NÔt”Œ´Ö_ÝØ>t¹eëìS­¯¦æžù^�\g¯õÎQe?ùvuöÌoïÕ;ˆï>ìˆ*ïwlò×mÑ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_24.png b/application/libraries/phpqrcode/cache/frame_24.png new file mode 100644 index 0000000..397c64f Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_24.png differ diff --git a/application/libraries/phpqrcode/cache/frame_25.dat b/application/libraries/phpqrcode/cache/frame_25.dat new file mode 100644 index 0000000..ba12518 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_25.dat @@ -0,0 +1,3 @@ +xÚíÛA +à …á¬s낋™]rƒx½Y51mMÈBG +ÿ¸*Sx|Ua5Ƶ‚Z—Š„-,Ž1ä²HÑPÒRj–šX5§®i†©’áG©>W¥ŽžRïöÕ/Ëâ+uT廯å Ïӯ嗴ªuæÏ¥Ú[Sía£[kví÷5•+5n§Á´JêÜ%+V¬X±bŊõ߬u'Á�±þÔû SRýå÷štzZ»ì+÷+V¬X±bŊ•ٟٟٟûŠ+V¬X±bÅÊìÏìÏìÏ}Ŋ+V¬X±ö±ª¤¥ÖVI©¢ÖÖ‘+k«qÿ[úËtŽ·oVZÍþvoNV³wÇ}µ{³r<ýR­Þ"�RÍÞ]ê W«r} \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_25.png b/application/libraries/phpqrcode/cache/frame_25.png new file mode 100644 index 0000000..25bc445 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_25.png differ diff --git a/application/libraries/phpqrcode/cache/frame_26.dat b/application/libraries/phpqrcode/cache/frame_26.dat new file mode 100644 index 0000000..d34a73f --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_26.dat @@ -0,0 +1,2 @@ +xÚí›A +à E³öօ,t§7ˆ7ћU� E)i7ï»*~cÃüÅÄXÖEBÆè°FC–˜³6¡:&çL,å¬Mv.ŽÂÎæKgŸոãYMç>ŸÎí>ûmۚ·?ª•vô¹¾mg?�ßұÎþ³æηªd˜“Cµ¹U¦ÏIk•ÚÚE\ÕÙMs†f˜a†f˜a>œ[sӈ9쬩ެ8böËî”]ßöLgÉÝÁ›³è¾äEo‚ w1 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_26.png b/application/libraries/phpqrcode/cache/frame_26.png new file mode 100644 index 0000000..f4a6b39 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_26.png differ diff --git a/application/libraries/phpqrcode/cache/frame_27.dat b/application/libraries/phpqrcode/cache/frame_27.dat new file mode 100644 index 0000000..b4d9ffd Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_27.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_27.png b/application/libraries/phpqrcode/cache/frame_27.png new file mode 100644 index 0000000..8419ec2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_27.png differ diff --git a/application/libraries/phpqrcode/cache/frame_28.dat b/application/libraries/phpqrcode/cache/frame_28.dat new file mode 100644 index 0000000..8cbaa19 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_28.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_28.png b/application/libraries/phpqrcode/cache/frame_28.png new file mode 100644 index 0000000..7609d8e Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_28.png differ diff --git a/application/libraries/phpqrcode/cache/frame_29.dat b/application/libraries/phpqrcode/cache/frame_29.dat new file mode 100644 index 0000000..5e4a711 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_29.dat @@ -0,0 +1,2 @@ +xÚíÜAƒ …a×޺ ‹™�Ü@n7+*¶šÖÚ4‘!Í?®Jšðò ³�”抮«]ª—ÉSŸâTf)–ÙsŠIÂ"…ȔbžÝ0…Š|•"Luٸî,Ž×EÇ1\6®*ÏuQÞ?¼>aÌυãþñŽÄRõ-r­“÷n.ïꯋ\®T¿ü:Ó*)|)°À ,°À ,þÑâêóåéx_ã¬}:^R„ƒUoɢ‰uÁ~ÁމX`�XЏЏЏЏ°_`�X`�XЏЏЏ°_`�X`�XЏЏЏЏ°wb�X`�¿¥PUõö)DÔÞ"cÈ{‹zçÎõ3ê›é<}¸ó¡^?b÷mÿÎÂ잃íº°»óaûŽ´’Âê.�] +³{Q6uáT,9 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_29.png b/application/libraries/phpqrcode/cache/frame_29.png new file mode 100644 index 0000000..ffe072c Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_29.png differ diff --git a/application/libraries/phpqrcode/cache/frame_3.dat b/application/libraries/phpqrcode/cache/frame_3.dat new file mode 100644 index 0000000..188d531 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_3.dat @@ -0,0 +1 @@ +xÚí“Á À E{vë&  à&°Y+¢b¤öÐkŸ'yù‘¤¿ÌÁa :äÀTXl�޶$W+ӏvû®9}gRæ¬@H0YPB½ÆÃEmÚÚ?ûœÍ±ísœÖ"bµìt2cnÖé†É:½ﺭë;¿Y§“ÃzÿQã«7¿Ô \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_3.png b/application/libraries/phpqrcode/cache/frame_3.png new file mode 100644 index 0000000..945ee7c Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_3.png differ diff --git a/application/libraries/phpqrcode/cache/frame_30.dat b/application/libraries/phpqrcode/cache/frame_30.dat new file mode 100644 index 0000000..44cf3d3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_30.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_30.png b/application/libraries/phpqrcode/cache/frame_30.png new file mode 100644 index 0000000..75dbddd Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_30.png differ diff --git a/application/libraries/phpqrcode/cache/frame_31.dat b/application/libraries/phpqrcode/cache/frame_31.dat new file mode 100644 index 0000000..ce429d0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_31.dat @@ -0,0 +1 @@ +xÚíÜAƒ …a×޺ ØÉ à&r³‚ Á´¸ªÎ4ù§«†´yù‚ķ!¥mV3I�µv­!ҜÖ2¢i\NSSä4EF2�+65Å¥‰e¾þÃ/Wœs]šñ¾‰!„Á?ÿpÅõû¦=S~ùüĐ?Ëý+þx¦Ö6r6yö³Ùƹ}“Ǵ™ë×eR1-�W•l°Ál°Á›ûҌÞXŸz/>V櫷ù§:ñÒÒÄAš8üý-+mTíÎÎbl°Ál°ÁlèštMº&]“³l°Ál°Áº&]“®I×ä¼Ál°Ál°Áº&]“®Éyƒ 6Ø`ƒ 6Ø`ƒÍÝi¬uy´ØXWòè±Éi¬²\t†ýz•—Š>•.î”z¾kÊß t²¿7©ß7òwJõϔ¶4Òw‘�҈ßÓÖÍ85‰ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_31.png b/application/libraries/phpqrcode/cache/frame_31.png new file mode 100644 index 0000000..b14d1fa Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_31.png differ diff --git a/application/libraries/phpqrcode/cache/frame_32.dat b/application/libraries/phpqrcode/cache/frame_32.dat new file mode 100644 index 0000000..aaa0808 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_32.dat @@ -0,0 +1,2 @@ +xÚíÜÁ +„ …á־õ€‹ë.ß ßDßl¬, ¦šMz‰ÿ6›†Ç gcJËD;ô'.®A’IqžމÄI,Ir¢Y¨»‘ËFk%‰DþOæy|EDªD×û(LÓ_Y�Ê>*ߚ?aÊOƒ¿k±L_£<[c—ñ¶ï>Êc˘õuÔLIäÕ%Â#Œ0Â#Œ0Â#ŒÞotѢšõµ}ÅÜ4Íf�v_)‰ÂE¢pú�¬h5R·�8�8³1Â#Œ0Â#Œ0¢ÓÒié´tZÎ#Œ0Â#Œ0Â#Œ0¢ÓÒié´tZÎ#Œ0Â#Œ0Â#Œ0¢ÓÒié´tZÎlŒ0Â#Œ0£÷9q"¢ÉHܜH™Qþ�µï"ÛÕL5}-ÝÜY׾Óê¸kì`¤â>¶z鸳®þÖ4&Òp÷á!‘Šû!«ù`¿:5 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_32.png b/application/libraries/phpqrcode/cache/frame_32.png new file mode 100644 index 0000000..58d42db Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_32.png differ diff --git a/application/libraries/phpqrcode/cache/frame_33.dat b/application/libraries/phpqrcode/cache/frame_33.dat new file mode 100644 index 0000000..a261375 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_33.dat @@ -0,0 +1,14 @@ +xÚíÜAƒ …a×޺‰‹™�Ü@n7+*L++Ó柮óò‰ÌbbÜ*LCï‘°‡‰c k™H¥rš”j•²�J5Yíi~0•_«òŒû×TÊTõ}å—e©>ýö5‘b_åwÐ͟?¿¤ßìæ§ÖÜù†\ý­RaÆi+7õßW©¦\ãþwLUNåL¦Â ++¬°Â ++¬°Âêÿ­jßÒO·ŸkcëÞñôç\˩|%•o<á‹k–­Lî+Î+Îv¬°Â ++¬°Â ++¬°Š>}ú ô8¯°Â ++¬°Â ++¬°Â ++ú ôè3Ðgà¼Â ++¬°Â ++¬°Â ++¬è3Ðg Ï@Ÿ�ó ++¬°Â ++¬°Â ++¬°:R‰¨ªX³ÚB‰9«”IÔ=çkÞ�±o/Swçؘ™ٯÏ`g¶áÅÊÌÈr_ٙ™Y¾ƒVSY™ÅzIefnmQoz >á \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_33.png b/application/libraries/phpqrcode/cache/frame_33.png new file mode 100644 index 0000000..924c728 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_33.png differ diff --git a/application/libraries/phpqrcode/cache/frame_34.dat b/application/libraries/phpqrcode/cache/frame_34.dat new file mode 100644 index 0000000..7ceb025 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_34.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_34.png b/application/libraries/phpqrcode/cache/frame_34.png new file mode 100644 index 0000000..a477042 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_34.png differ diff --git a/application/libraries/phpqrcode/cache/frame_35.dat b/application/libraries/phpqrcode/cache/frame_35.dat new file mode 100644 index 0000000..56bc3e2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_35.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_35.png b/application/libraries/phpqrcode/cache/frame_35.png new file mode 100644 index 0000000..d29806c Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_35.png differ diff --git a/application/libraries/phpqrcode/cache/frame_36.dat b/application/libraries/phpqrcode/cache/frame_36.dat new file mode 100644 index 0000000..282c60d Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_36.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_36.png b/application/libraries/phpqrcode/cache/frame_36.png new file mode 100644 index 0000000..96ecb42 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_36.png differ diff --git a/application/libraries/phpqrcode/cache/frame_37.dat b/application/libraries/phpqrcode/cache/frame_37.dat new file mode 100644 index 0000000..015c0f2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_37.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_37.png b/application/libraries/phpqrcode/cache/frame_37.png new file mode 100644 index 0000000..fcc5162 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_37.png differ diff --git a/application/libraries/phpqrcode/cache/frame_38.dat b/application/libraries/phpqrcode/cache/frame_38.dat new file mode 100644 index 0000000..71cf53e --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_38.dat @@ -0,0 +1 @@ +xÚíÝAªƒ0ЎÝuÁA2«;НèÎkü(üg¾Ày•tp9Äï$Ëò™¹Dœ”ò¼\ºe^'tÒ-aIºŠFMšSškÂðIóŤÓ:7®¤|LúkŸNã8N7®œöi}ö‡×Ÿi,Ÿ[W†¿g®Ӵ̰ë?3ô1÷i™¾N·}}=ÂOM:4“”)S¦L™2eʔ)S¦L#$½ÿ ôÂJã­þÂJM:}ý]˜•ÖL›٧ÎSÿQL™2eʔ)S¦L™2աèPt(:Šó”)S¦L™2eʔ)S¦:ŠE‡¢Cqž2eʔ)S¦L™2eʔ©E‡¢Cѡ8O™2eʔ)S¦L™2eÊT‡¢CѡèPœ§L™2eʔ)S¦L™2ݓ¦”sJCIKÖԂiÍ93�ônº_Ñò ÿ¾¿ü¼“+R‡û®£“ièû£Žû4ö�\Çg¿¥¤‘ïŽ;% }ßaÞnŽ£ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_38.png b/application/libraries/phpqrcode/cache/frame_38.png new file mode 100644 index 0000000..89238f3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_38.png differ diff --git a/application/libraries/phpqrcode/cache/frame_39.dat b/application/libraries/phpqrcode/cache/frame_39.dat new file mode 100644 index 0000000..53511f7 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_39.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_39.png b/application/libraries/phpqrcode/cache/frame_39.png new file mode 100644 index 0000000..1dc9cd1 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_39.png differ diff --git a/application/libraries/phpqrcode/cache/frame_4.dat b/application/libraries/phpqrcode/cache/frame_4.dat new file mode 100644 index 0000000..67b30e8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_4.dat @@ -0,0 +1 @@ +xÚí”Á À E=»uÐ pجQ•ØCOMŸ'Ãˏ$ ³@à¨Ø3e–F©\FNXRyÉؾC{‰a8­Ræ Ńa2@ñ圉qküßÉH1ê(£�ˆÅ`cç¦j³~Ë0ö¥¿ÃܨÖËÃعnXÿGåÿ�Ā \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_4.png b/application/libraries/phpqrcode/cache/frame_4.png new file mode 100644 index 0000000..b72f9e7 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_4.png differ diff --git a/application/libraries/phpqrcode/cache/frame_40.dat b/application/libraries/phpqrcode/cache/frame_40.dat new file mode 100644 index 0000000..90d36dd --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_40.dat @@ -0,0 +1,2 @@ +xÚíÝAŠƒ@Ь½õ€‹î�Þ@o¢7“˜`“Qfeº䕫PA>�¦ÀÚôã°<]ßÕÐë߳bZ«nã¥^A›öQ}[÷9^ª]«yþìnajM܇K̘1cƌ3f̘1ã¸Æ{ßW5}ç½{ÍÑ7lMßÒïޚxÜI<¼áK½¨Æáαyl3f̘1cƌ3f̘1ã«ۻٻ={·“αyl3f̘1cƌ3f̘1ã«ۻٻ={·“αyl3f̘1cƌ3f̘1ã«ۻٻ={·“αyl3f̘1cƌ3f̘1ã«ۻٻ={·“αyl3f̘1cƌ3f̘ñ÷çœSʑŒӒ7¥HÆK޼g\ç¾âuõßÏ_ÿªr'4Ü[çÞ-Æ]›…qˆûL·ç8Ɲ±ÛY1q„»�‹Ä!î—ÞÔ/(%û \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_40.png b/application/libraries/phpqrcode/cache/frame_40.png new file mode 100644 index 0000000..8034d86 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_40.png differ diff --git a/application/libraries/phpqrcode/cache/frame_5.dat b/application/libraries/phpqrcode/cache/frame_5.dat new file mode 100644 index 0000000..d5dafe1 --- /dev/null +++ b/application/libraries/phpqrcode/cache/frame_5.dat @@ -0,0 +1 @@ +xÚí”1À E�½u 7ЛÀÍZµ‡|N†—üD B0@R$l,-™>VKZ[<ýØÚz—qƎ¨ØYJ&ƒi�嚂‹ZyË:Y'ë¯YµÁVÿ&—e•RÄ"§sj©Ýrþö+Ëé‰ù.·MƎ»–Ó9ÓòzµsŽ”É, \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/frame_5.png b/application/libraries/phpqrcode/cache/frame_5.png new file mode 100644 index 0000000..96b6494 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_5.png differ diff --git a/application/libraries/phpqrcode/cache/frame_6.dat b/application/libraries/phpqrcode/cache/frame_6.dat new file mode 100644 index 0000000..0fc3d03 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_6.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_6.png b/application/libraries/phpqrcode/cache/frame_6.png new file mode 100644 index 0000000..05ca358 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_6.png differ diff --git a/application/libraries/phpqrcode/cache/frame_7.dat b/application/libraries/phpqrcode/cache/frame_7.dat new file mode 100644 index 0000000..4337596 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_7.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_7.png b/application/libraries/phpqrcode/cache/frame_7.png new file mode 100644 index 0000000..7d2ff4f Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_7.png differ diff --git a/application/libraries/phpqrcode/cache/frame_8.dat b/application/libraries/phpqrcode/cache/frame_8.dat new file mode 100644 index 0000000..669b325 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_8.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_8.png b/application/libraries/phpqrcode/cache/frame_8.png new file mode 100644 index 0000000..db1f187 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_8.png differ diff --git a/application/libraries/phpqrcode/cache/frame_9.dat b/application/libraries/phpqrcode/cache/frame_9.dat new file mode 100644 index 0000000..d79295e Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_9.dat differ diff --git a/application/libraries/phpqrcode/cache/frame_9.png b/application/libraries/phpqrcode/cache/frame_9.png new file mode 100644 index 0000000..74ddf08 Binary files /dev/null and b/application/libraries/phpqrcode/cache/frame_9.png differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_101_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_101_0.dat new file mode 100644 index 0000000..51deaba Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_101_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_105_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_105_0.dat new file mode 100644 index 0000000..97e9e5d Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_105_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_109_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_109_0.dat new file mode 100644 index 0000000..eadf83a --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_109_0.dat @@ -0,0 +1,2 @@ +xÚíÚ= +€0 н§iï9'Åb‡$ ¾tËýáÚû^#i�ª¥Ëi?³ÅôÛbúK[AUØF徝Ƶijx]mŸ]2Ž������-Ė�KŽ~ ÏVw}¶X›ûÆÆÆÆÆÆÆ&O²ɓÞ666666yRž”'½%lllll/´åhœl…Ãîm ¹¤ê�ádël™¶´3Ù+ïÛmͫ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_113_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_113_0.dat new file mode 100644 index 0000000..5eb7f5d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_113_0.dat @@ -0,0 +1,2 @@ +xÚíÚ; +…0Ð>«Iö¿9+Eñƒ‚sá=ϤL1̄[¤÷¹FáZU‹4‡?i<ÿ�ç;7�çòç;‡ÆP¥Œ#ýW-[ñݯ6÷�µddddddüc",;í"�¼Ÿsk�摑‘‘‘‘‘‘Q&—Éerw######£L.¯Êä摑‘‘‘‘‘±Иy¼1†^˲\ò�îÆØ3Æâ³ÚÓóøÏÆ ‘Ñv \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_117_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_117_0.dat new file mode 100644 index 0000000..781c7f8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_117_0.dat @@ -0,0 +1,2 @@ +xÚíÚA +„0 Ð}OÓÞÿr³R,#3öñ¥Ë,âÃþ¢½o5ŸCµØÐq:õõÖ;;¬wvN­ÁJZGÅ=Œm�»û}Úö ѱ¬¬¬¬¬¬¬¬µ¦2âÞi‹RïïkÆj_YYYYYYYYeÙ_ö·¯¬¬¬¬¬¬¬¬²¿ì/ûÛWVVVVVVÖkîáýd-úϺ,#¦ßÀOÖÎZc]|‡{ž¾Áúˆ­$™ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_121_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_121_0.dat new file mode 100644 index 0000000..68810c3 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_121_0.dat @@ -0,0 +1 @@ +xÚíÛ1„ ОÓÀý/·•w YMüSø Ä8>2SÐûÖF O­E�ÓÈÌÓc«W\ûۼš‹{c§æpK›GÕžB·КþímûŽxhfffffffæ/s2ÛÇÚ2W|*÷d®1ÛÏÌÌÌÌÌÌÌÌÌê*5³ºÊÆÌÌÌÌÌÌÌ̬®RW©«œaÌÌÌÌÌÌÌÌsöòËÁ\xm~8߮¸ƒr0wæjsdm÷ªýü&óâyÙ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_125_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_125_0.dat new file mode 100644 index 0000000..2c73ef1 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_125_0.dat @@ -0,0 +1,2 @@ +xÚíÛA +€ н§Ñû_®•‘TH`3AO—L�4ü kí£ÍÃ(áÍÛewö›GWÖÙ××.í #ÃÞ2¿û¡} \ëçYú»¤´gggggggggg_d�>Ïïµòj^™ÕØsíö;;;;;;;;;;»'ÇÉqö;;;;;;;;;»'˰ËqþuììììììììµÇ_Pì‰Íçö—ÏóYwÄ{eÿš=dÍ×ÌýþGû/ý“¸ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_129_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_129_0.dat new file mode 100644 index 0000000..812ee8a --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_129_0.dat @@ -0,0 +1,2 @@ +xÚíÛ1 +Å ÀÞÓèý/—*Ÿ„DøEØ'ÉhgõtÙ-ì}_£pV‹· \"Ìb=sþ—ÁÌå™ó[ƒÐJŒô=8DhÅoàº۞'� 0XÐ ÑüÎ۴�©è‘¬e 0`À€ ˜ôÊ Ì j" 0`ÀÀ¼ Wf`^P0`À€æ½2ó‚šȀ ¼Ò óÉôd07(ì“Y‹/XLÿGb…yÁ"êpT� \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_137_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_137_0.dat new file mode 100644 index 0000000..f6d993b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_137_0.dat @@ -0,0 +1 @@ +xÚíÜ1„ ОÓÀý/·•+FÉZ?®JšÉ Læ7Ժ¬ØÝ*ÑBÚa%L~šŒ˻òä”ÉØéʓC“ðJ›´YîIWJ ½™ý.K]ñR˜0a„ “›$ç“ïINTwÎlLža¢Ÿè±L˜0a„ &Ld@PÔO˜0a„ &L˜0‘e@P?a„ &L˜0aÂD”e@ý„ &L˜0aÂäÏM²ŸIlL&)dl˜ígøÃacR™<É$övê,ý„ɺ?U2ç] \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_141_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_141_0.dat new file mode 100644 index 0000000..8c685c8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_141_0.dat @@ -0,0 +1,2 @@ +xÚíÜ= +€0 н§iï9'EÑúÖDx͘%«Iö¿9+EñÇ{ˆõ$åm†ƒ^˜&µŽ§u¼³S"µÍDŒþ6ڟ]9ùÉè8ìU“M£�“bÔҾ£Y¤Òù[ß2拉Ĉ#FŒ1bĈ£Ï%ôµiRNš潝–ѳ�ì#;›#FŒ1bĈ‘N«¯1Òií#FŒ1bĈ#FŒtZ}�‘Nk1bĈ#FŒ1b¤ÓêkŒtZ;›#FŒ1bÄèõF�V-ŒÂ�uè"IoD-Œ*£7uû×jÚ>b´MV“+ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_149_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_149_0.dat new file mode 100644 index 0000000..d258350 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_149_0.dat @@ -0,0 +1,3 @@ +xÚíÜA +€ н§Ñû_®MEQXP৞.Ý|94³°ÖeµÁ{³JL¨všŠÕ#«^Ü÷Ïn[õ ß?;µ +ZIV-ñ»ÚÄ*wï¸˒1*+V¬X±bŊ«ßX¥ü·¯g¥ÛwŒèqX}ÇJ½RÛY±bŊ+V¬X±beΠwfeΠ^±bŊ+V¬X±bÅʜAïÌʜA½bŊ+V¬X±bŊ•9ƒÞٜÁœA½bŊ+V¬X±bŊÕl•ó0ÞÎ*0Tßj`?˜öÝΪ²ú¢Õð;Xë«ë=zZr* \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_153_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_153_0.dat new file mode 100644 index 0000000..fc79e9e --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_153_0.dat @@ -0,0 +1 @@ +xÚíÜ1€ НÓÀý/礑h¢&F¾ú`ìҼ@I;Pë¼ZÀ^­•XÛ͌ÙmfÏÆ.™=Û5 [if-õœ­R+!wr»˜g\j̘1cƌ3f̘1cf–Ôo.±2¬?Š1ûž™zæ `ƌ3f̘1cƌ™¹†�™¹†zƌ3f̘1cƌ3fæztfæê3f̘1cƌ3f̘™k˜k03×ð0cƌ3f̘1cö³¬Ï9;³ÐĎÍö›‰`vf•Ù̆ß͚ZϘ�ß¶W9 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_157_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_157_0.dat new file mode 100644 index 0000000..ad749f3 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_157_0.dat @@ -0,0 +1,2 @@ +xÚíÜA +€ н§Ñû_®•QRù©§»Y ùkí«…ìÍ*q͵ÓîØ=j7§~ÙnNýÔ.p%ڵäs·i¯ÝÕã.½×Èöرcǎ;vìر{Ù.-W¬õ2={�êì¾mgÞy+رcǎ;vìØɳ2;yּcǎ;vìرcÇNž•ÉØɳæ;vìرcǎ;v2™<ËNžõV°cǎ;vìر“ge2vò¬yǎ;vìرcÇî¦]ÞÂ;»àæÆv¹"õ�ޝ]e÷'»˜;[“ç»û{¾|Aµ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_161_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_161_0.dat new file mode 100644 index 0000000..4bdc5fd Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_161_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_165_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_165_0.dat new file mode 100644 index 0000000..3a17a05 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_165_0.dat @@ -0,0 +1,2 @@ +xÚíÝ; +€0À>§Iî9+ÅàDyÁIÊ4ˠ÷5Ö:¯¶W«ÄÙv«dù¢åqñߜܴ<þæd×2x%[¶žËU™%ðÝÞî2×]&K–,Y²dɒ%ˡ,S¿՗“r2ydÌ=,ÿk©_º{X²dɒ%K–,Y²d)Û0�³”mè—,Y²dɒ%K–,Y²d)Û0�³”mè—,Y²dɒ%K–,Y²”m˜Çe,eú%K–,Y²dɒ%K–,eæq–² ýÒÝÒ%K–,Y²dɒå…eî�:ËŠ<³Œš!Óÿ¡ÐYV–,ßñ:B¿dù|O¨$*# \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_169_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_169_0.dat new file mode 100644 index 0000000..c4787d9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_169_0.dat @@ -0,0 +1,2 @@ +xÚíÝ1 +€0À>¯Iþÿ9EQÑ=˜¤Ls æä¶IïË�{³Zt¡ã´R¦™þ}òÀôï“SÓð•n:ª|§›R[è�?î¶Ô_*S¦L™2eʔ)S¦L™™&ϦëI šê¯O˜2ÕOý£˜2eʔ)S¦L™2e*C1ïËP˜ÊPôS¦L™2eʔ)S¦L™ÊPÌû2¦2ý”)S¦L™2eʔ)S¦Le(æ}¦2ý”)S¦L™2eʔ)S¦Le(æ}¦2ý”)S¦L™2eʔ)S¦³iöc7;Ó"…ޙFͦސٙv¦L Üý^¥Ÿ2}oOäß'r \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_173_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_173_0.dat new file mode 100644 index 0000000..5ef85e7 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_173_0.dat @@ -0,0 +1 @@ +xÚíÝ1ƒ0Àޯ±ÿÿ¹T ¥ˆ¬[4véæ2ŠØƽok݇ÕÊ;¦Ӳ]fûô֞ýdûä½ölj°lGÒÿö0n+œ߻m³GŒ˖-[¶lٲeû"Ûêï·ûY»}o¯öíÀ–íÕVÞÊ[¶lٲe˖-[¶lٲe«ÓհՃÉ[Ï2¶lٲe˖-[¶lٲe«ÓհՃÉ[Ï2¶lٲe˖-[¶lٲe«ÓհՃÉ[¶lٲe˖-[¶lٲeËV¦«a«“·lٲe˖-[¶lٲef[ÿB°“mа÷¶Eû„”;¶N¶�-ۜT¸/r’·lÿ¿?ØÔ*Ñ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_177_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_177_0.dat new file mode 100644 index 0000000..78a26a7 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_177_0.dat @@ -0,0 +1,2 @@ +xÚíÝ1 +…0Ð>§Iî9+?вØè߁—”iև dš̹¯ÕxֈxÝNÌø/Æէ|{öظ²ÿöìÖ8d¥¯´ÿø0òhž×=öùcFf̘1cƌ3f̘q¼qÂ=ïw6Ê;l·»4cƕ±<–nj3f̘1cƌ3fÌXï¦һ1ֻÉcƌ3f̘1cƌ3fÌXï¦b¬w“nj3f̘1cƌ3f̘±ÞM'ÄXï&�å1cƌ3f̘1cƌ3ֻé„ônŒõnò˜1cƌ3f̘1cƌÍ3U<‡ \7í+’Þ(<OƌŸ·Ί™–njßÛÕä4@ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_21_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_21_0.dat new file mode 100644 index 0000000..368c994 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_21_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_25_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_25_0.dat new file mode 100644 index 0000000..e4a5b6d Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_25_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_29_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_29_0.dat new file mode 100644 index 0000000..74a216b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_29_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_33_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_33_0.dat new file mode 100644 index 0000000..2ec712a Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_33_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_37_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_37_0.dat new file mode 100644 index 0000000..1588cfc Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_37_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_41_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_41_0.dat new file mode 100644 index 0000000..e369027 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_41_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_45_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_45_0.dat new file mode 100644 index 0000000..452f126 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_45_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_49_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_49_0.dat new file mode 100644 index 0000000..fdd2aac --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_49_0.dat @@ -0,0 +1,2 @@ +xÚíÕKÀ EÑ9«�ýo®#?H/›6g$Ï-�ª­,X] +˜›xݘ;Àð·˜ X԰ì9ºˆ<ÜѺ¤åq¤Œ2 Af÷îHð7ø�/5We„{Ô#ØáìfިÁÐ?à®4û=N >Çæ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_53_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_53_0.dat new file mode 100644 index 0000000..572d279 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_53_0.dat @@ -0,0 +1,2 @@ +xÚíÖK +@!й«Ñýo®QÑϺ:(m&ܯžs-¹¬®É6õÑZ{�¥m4Y†²Xâù®.FÀ٭‹ê¦XZi·Ðùj=:έ֋b忑VH 8 –#¤[—ù²¾‹ãYвç¾^XÔ÷e \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_57_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_57_0.dat new file mode 100644 index 0000000..ea81e6d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_57_0.dat @@ -0,0 +1,4 @@ +xÚíÖA +À À{^“üÿs=YL՚ ( +o�u¨– j) ¬ª +Z7y„ÙëÍñv,ԴìwVQ ži»¤ìGiҤ¹ÅDfÂەÉÄwo4ùѤ¹ÐÄoÎLÿÌéLȼÁœ©·›Á³Õì}î4� hå \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_61_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_61_0.dat new file mode 100644 index 0000000..93d2444 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_61_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_65_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_65_0.dat new file mode 100644 index 0000000..df29d7b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_65_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_69_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_69_0.dat new file mode 100644 index 0000000..8a2cfbd --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_69_0.dat @@ -0,0 +1 @@ +xÚí×KÀ Ð=§�û_®+­m�ÏBìà’d|Q"s+1®®È"¯),ß=“EÛàa TÄ"çÒŐãnæE-3 ,°ÀKY‹wÝ=ZšZïT Þ.,°ÀK1‹ÿ#֞!�Ŋ+V¬嬪½õöí÷.Ýõ±2¯X±bŊ•ÿ¬Ì+V¬X±–²ê.ökBèÞz±·Ôwðƒձ¦̀gÌkë¡YZý \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_89_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_89_0.dat new file mode 100644 index 0000000..aaa4c52 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_89_0.dat @@ -0,0 +1 @@ +xÚíÙ1€ …ὧiï9'Œˆ€Ñ×ýËHl?Lšà^"ë&M—™?bî—qÃÜ?˸äÉ,µ9²þó!µ‰z¢]VêÈScƌ3æ_š•÷à¾cÓé!î`nÍô3f̘1cÆ̌„™ 3f̘1/fÖ>.Uæ¤Äcó˻;ã ¨2;æ™YÒžÕÏ+™7Þý \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_93_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_93_0.dat new file mode 100644 index 0000000..e218fa0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_0/mask_93_0.dat @@ -0,0 +1,3 @@ +xÚíÙK +À EÑyV÷¿¹Ž,ÚO¬mޠrãPHŽ0 {�’´š°ôäå2ûbö¨ìýÞc{t¦Qáƒ] +{QÞ{“Þ{弬֒¤ǎ;vìØ_ڳßÕ}ÏÂ÷ýËL€}l§߱cǎ;vì̑̑̑رcǎû¢öüÐÎ.LÛæÕegwì3ö´žqe¿ÿѾ@�i \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_0/mask_97_0.dat b/application/libraries/phpqrcode/cache/mask_0/mask_97_0.dat new file mode 100644 index 0000000..74ac719 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_0/mask_97_0.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_101_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_101_1.dat new file mode 100644 index 0000000..ec939b5 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_101_1.dat @@ -0,0 +1,2 @@ +xÚíÖ1 +À н§‰÷¿\Q÷ªEóþ¢d  1¢¦ÌNô<±#½ËՖ¯Õ-7u™þ.ÃÓl²ԦeiXXXXXRZVíÊVeIo1û,,,,,v%‹?ŒÙgaaaa±÷YÌ K&K=/·œ·+ۍå˱ó²Öò¼ގã \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_105_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_105_1.dat new file mode 100644 index 0000000..e1f5c99 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_105_1.dat @@ -0,0 +1 @@ +xÚíÖ1€ ÐÝӔû_Υ‡b€÷ KóB¬?"Ó*#WÌʘt€éÍùíügӎ“JîéqUM9¸•‡‰‰‰‰©ÔTùêçLLv“¾ǤïÙLLLLLLz“¾gG01111šòy„iåÑߘ–4mð=՛n§¥+2 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_109_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_109_1.dat new file mode 100644 index 0000000..7e0d6d1 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_109_1.dat @@ -0,0 +1 @@ +xÚíֱ À À>Әý—Kƒ€°•À}“ˆÆ:!£�èiY‰™'*3§]fÛsþÛÉÓÒîm¹ºb[ÞJÂÆÆÆÆÆƶŖÝKÆ9›}ccccccÓ'u.6ûÆÆÆÆÆÆƦs±é“Þ6666¶R[ÿ^gû{/lÇØٷÛ Þ7͂ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_113_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_113_1.dat new file mode 100644 index 0000000..1dd666d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_113_1.dat @@ -0,0 +1 @@ +xÚíÖ1€ ÀÞ×ÿÿœ -8fL(pBŽlDM™è9";ýÄ-�÷ò÷;?1îpâÐ{¼\åÆú%-ŒŒŒŒŒŒ“3:@Ûad4�ŒŒŒŒŒŒŒŒú*£NîÍaddddddÔÉÍ#######ã#c]75®ÓåÚã¢Æåæ1ÏxYuñ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_117_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_117_1.dat new file mode 100644 index 0000000..8921f64 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_117_1.dat @@ -0,0 +1,2 @@ +xÚíֻ À À>Әý—Kƒ€ø$ø^’ 8Y–QSV'zž8‘þjzëÌʇ¬™^]Þסµ‡¬õekXYYYYYYYjݵ#¶ ++«yeeeeeeeeµ#²²šWVVVVVVVV;"+«yeeeeeee�l­'ëe;b»±&²^9¯{­/ÊJ$p \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_121_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_121_1.dat new file mode 100644 index 0000000..64bd8ba --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_121_1.dat @@ -0,0 +1,2 @@ +xÚí×1 +À н§‰÷¿\— ¥C¡PbїEÁÁ$’DdƩÄÊ•Y»³ÿŸtëڅöλ»÷ÏÕÛó0£$ìììììììììì›ì§ûù¹ÆήÞÙÙÙÙÙÙÙÙÙÙõóììꝝ�������Ýg–a7ÇyëØÙÙÙÙÙÙٯ³痽Y??ÿØÙ{Õ{�ý¾D“� \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_129_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_129_1.dat new file mode 100644 index 0000000..62cd1c9 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_129_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_133_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_133_1.dat new file mode 100644 index 0000000..18d68dc --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_133_1.dat @@ -0,0 +1 @@ +xÚí×1€ ÐÝӔû_΅€“ºh«¼¿h˜šÒ|"zÚۉ™-*dNÁâÔâÙó¦H¼‡«QĢÕR ,X°`ñc‹¬Þ9ÎY°(na_°`Á‚ ,Xè�,XØ,X°`Á‚ Þ#:8 ﻓ ,X°`ÁBïdÁ¾`Á‚ ,X|ϢY\X¬Ö;Ç 7-ݹ;� ` \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_137_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_137_1.dat new file mode 100644 index 0000000..284d7be --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_137_1.dat @@ -0,0 +1,3 @@ +xÚí×1 +€0 ÐÝӤ÷¿œKh]D¥‘¾,-t �ò#²ڌŠQ[T©Ñ “K“s§7_Š›è¤È?9|•B&ÙX›^L˜0a„ÉÂ&3÷“þ„ÉMÌ&L˜0a„ &2 ¼ÃD4c™0a„ &L˜ȀòЌe„ &L˜0ab·—w˜Ȁf,&L˜0a„É7&y2¹a²ònßoL˜<01O +˜ì¾ç \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_141_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_141_1.dat new file mode 100644 index 0000000..83220dd --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_141_1.dat @@ -0,0 +1,2 @@ +xÚí×1 +À À>¯9ÿÿ¹4¢6)ƒ3×$`s ²¸uʮ‰>Wdš¾ ›)›g³•'°±MÂ{3\�d6u¹’bذaÆ 6lؼn³û]ÜNذ9ÄFްaÆ 6lذa£3°a#oذaÆ 6lذÑذÑ5e16lذaÆ ]SŸb£kÊ6lذaÆ ›mê—ͤÍß;CûcÃf‘�¼Ids�G¹ÿ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_145_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_145_1.dat new file mode 100644 index 0000000..6a9950f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_145_1.dat @@ -0,0 +1 @@ +xÚíÜ!€0@Ïk®ÿÿ¦¡ 4a)³qæ2i.YCUO{35²UZÆFŒn]ÏfN>bd£ðwtzJ�F}ÁFŒ1bĈ#FŒýÆ(¡¯FŒ6r�1bĈ#FŒ1ÒE1ÒiÝlFŒ1bĈ#FŒtFŒÜ#FŒ1bĈ#FŒtZ}�#÷ˆ#FŒ1bĈÑl£þeôÀHÉùG£µ�ܣ@£ٝ’î \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_149_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_149_1.dat new file mode 100644 index 0000000..02a3cdc --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_149_1.dat @@ -0,0 +1 @@ +xÚíØ1€ ÐÝӔû_΅ˆƒqÁHÞ_Xšòc‰èi«#Gd̘ŠՔ՛á÷g…¬LUè^ݮVR«>dKV¬X±bŊ+VÛXeùo¿ÎX±ÚÌJ_±bŊ+V¬X±bÅÊ;ƒݙ+}Ŋ+V¬X±bŊ+VöAV¬ô•ngŊ+V¬X±bŊ•}�+ï úŠ+V¬X±bŊ«VýËjÒÊ>øhewfõ›•¾*`uTqí \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_153_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_153_1.dat new file mode 100644 index 0000000..2abfca2 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_153_1.dat @@ -0,0 +1,2 @@ +xÚíØ1 +€0ÀÞ×\þÿ9›Ãbt³�Ešc'›ªÎHHÍ•š9³efߞmff²MïÙíª›õ #.̘1cƌ3f̘1cf–Ô7¯3f̘Ùg̘1cƌ3f̘1c¦o2cæ]Ã?€3f̘1cƌ3fÞ5ôMfÌì3f̘1cƌ3f̘1Ó7utfÌì3f̘1cƌ3f̘=ÌúËl¡™¾ùj¦£3ûÕÌ>ÛÈì¤ÐVü \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_157_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_157_1.dat new file mode 100644 index 0000000..17344b8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_157_1.dat @@ -0,0 +1,2 @@ +xÚí×1 +À À>¯9ÿÿ¹4S¦�d/Ì5‚Õ1ÈÊVÍ)SkŽJžµ»­vßÜ7´³ÝÞÝíé…ÛÍeGä°cǎ;vìرc÷²]Z¯¸îٱc'ïرcǎ;vìرÓ+رÓgå;vìرcǎ;}V¯`ÇNÞù+رcǎ;vìØé:;vòŽ;vìرcǎ;½‚;}Vޱcǎ;vìص±›'»ÍvzÅ#;�Œ]Œ�¼klw¯oA` \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_161_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_161_1.dat new file mode 100644 index 0000000..669ade1 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_161_1.dat @@ -0,0 +1 @@ +xÚíØ1€0ÐÝÓÐû_΅XÓ¨�š yi~…ˆ¬Q©bÖÕkvÈp±á³ê7'M u¸Ù=¼]ņÙð([ 2dȐáÏ +þÛ\' 2”‡ 2dȐ!C† 2´s0/3d(½) 2dȐ!C† 24ë1dhç 2dȐ!C† 2dhÖcȐ¡ÊS¦L™2eʔ)S¦L™2­œMç S¦LÍS¦L™2eʔ)S¦L™2•Må}¦LÍSS¦L™2eʔ)S¦LeSyŸ)SóÔŔ)S¦L™2eʔ)S;ٔ)S;ó”)S¦L™2eʔ)S¦v(²)S¦v(æ)S¦L™2eʔ)S¦íLódºÍT6}a*ï3mljžmzC' \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_173_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_173_1.dat new file mode 100644 index 0000000..436918c --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_173_1.dat @@ -0,0 +1 @@ +xÚíØ1€ ÐÝӔû_Υ''Ó@y]Xšò1?"g¬:1犝fn˶Ìö˻ö¬�­m¼·¯«»™m.?¶¶lٲe˖-ÛF¶«ÿß>glٲ•·Þ2¶lٲe˖-[¶lٲe«O`˖­¼e˖-[¶lٲe˖-[¶lõ lٲ•·lٲe˖-[¶lٲeËV¦O`˖­¼e˖-[¶lٲe˖-[¶z0}[¶z0y˖-[¶lٲe˖-[¶E¶ùe[h«OøÍVWö…­¼=Ìötÿ*| \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_177_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_177_1.dat new file mode 100644 index 0000000..12e2e52 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_177_1.dat @@ -0,0 +1 @@ +xÚíØ1€ ÀÞ×ÿÿœÍE¨¬Œp0×XÐà„,a#rÚÊ}®¨6}nj§¿Ùÿ»¶‰±~އ£\Ð8 •ƌ3f̘1cƌ—7®ðÎ{Ö3f,�ÝyŒ3f̘1cƌ3fÌX_¡¯`ÌXï&�3f̘1cƌ3f̘±ÞM_Á˜1cy̘1cƌ3f̘1cƌë+3f,�ÝyŒ3f̘1cƌ3fÌX_¡bÌXï&�3f̘1cƌ3fÌxãü2žd¬¯øÔX'Äx[cy|¨ñ ¸•3ë \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_21_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_21_1.dat new file mode 100644 index 0000000..f87e0a1 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_21_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_25_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_25_1.dat new file mode 100644 index 0000000..3a225e3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_25_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_29_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_29_1.dat new file mode 100644 index 0000000..0a1cb3b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_29_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_33_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_33_1.dat new file mode 100644 index 0000000..318949d Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_33_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_37_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_37_1.dat new file mode 100644 index 0000000..5bd9e3a Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_37_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_41_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_41_1.dat new file mode 100644 index 0000000..52e9e58 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_41_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_45_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_45_1.dat new file mode 100644 index 0000000..b35c567 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_45_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_49_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_49_1.dat new file mode 100644 index 0000000..d20d717 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_49_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_53_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_53_1.dat new file mode 100644 index 0000000..a676d7d Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_53_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_57_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_57_1.dat new file mode 100644 index 0000000..896ed43 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_1/mask_57_1.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_61_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_61_1.dat new file mode 100644 index 0000000..4165a4b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_61_1.dat @@ -0,0 +1 @@ +xÚ30€Cb�pPº™ÝØÅi`÷@ê&ÚßH^§Ðn¨a†d�Q»Gíµ{Ôn§Ñû_.�Ä4I‘ìâ‡y킎¢¸`)-5*¥ç(™é£of[Ósm€}åÑÃöý¶õÉö6YM ;;;;;ûG{ô»zÕطµ»«ìììììììz1vw•�����}œ=úwuûìýLÆ%û?û"ç=Ç~e—i \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_1/mask_97_1.dat b/application/libraries/phpqrcode/cache/mask_1/mask_97_1.dat new file mode 100644 index 0000000..24fa60f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_1/mask_97_1.dat @@ -0,0 +1,2 @@ +xÚíÙ1 +À0н§1÷¿\—ÐtncKÌû«ƒ‰D"‘H$ÊäD™œH$‰D"Q&—WerûH$‰D"ñ±ç*x[±(Ë?/ÚÇ÷ˆ'ŸŠnd \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_117_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_117_2.dat new file mode 100644 index 0000000..b4dcce4 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_117_2.dat @@ -0,0 +1,2 @@ +xÚíÚ1 +À À>¯9ÿÿ¹4!m dOƒs\§0X,laÕ5#¿÷E�ª¾>Z[íœַŽÉêã½ÄR©T*•J¥R?Q-œº«*¯T*•J¥R©º?U÷—W*•J¥R©TݟªûË+•J¥R©ԤÚùó~m5Õ©;ªòúSõ&+ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_121_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_121_2.dat new file mode 100644 index 0000000..a2a0097 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_121_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_125_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_125_2.dat new file mode 100644 index 0000000..0ea40fd --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_125_2.dat @@ -0,0 +1 @@ +xÚíÛ!À P¿Ӕû_ƒ@ U(Ûkp@^Mú£ڮ5ÕôªúËîòâ-ú‰:ðöVF_žÌ\t:�N§Óétú÷õ¼yžN×qt:�N§ÓétºG—ãô;�N§Óét:�.ËÐå8:�N§ÓétzA}ÿµ‹ôÔyžN×q¿Ñ;+nŠ& \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_129_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_129_2.dat new file mode 100644 index 0000000..bf04839 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_129_2.dat @@ -0,0 +1,2 @@ +xÚíÛ1 +À0н§Ñû_®KÈVڡ¨�'.Éôþ!«²¶w]A0XðÄú÷~˜ «ÚÞ  ö!›š€€€€€€࣠fK# x˜Fy�€À4È vey�€€€€€€€@^°+È ç º~™ž ¨Û L#ÁêveI� \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_133_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_133_2.dat new file mode 100644 index 0000000..9e78b6d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_133_2.dat @@ -0,0 +1,10 @@ +xÚíÛ1 +À н§‰÷¿œ‹à&´`¢ô…Lâð”üÁˆQ-½g=Aq•bëú¹Šʪ<‹všb½+©)((((((((ÊIën*…÷­l𠆆fŽƄÓÐÐÐÐÐÐÐÐÐȚ44²& �¬)OÑȚÞY“FÖ4á444444444c4~œ9S³:3ÐЌטpšǮ>® \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_145_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_145_2.dat new file mode 100644 index 0000000..9ff2bbf --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_145_2.dat @@ -0,0 +1,4 @@ +xÚíÜ1 +À н§ùÞÿr] +Ý,tˆQ^È&ÊCˆð“§ÆÒ~ë +щ¢Éj~mɾ¢.ÕåŽFgÑüìMDDDDDDDDDDDÔST™׈ˆêD¦ŸˆˆˆˆˆˆˆˆˆH¦•ŽˆdZï‘L+¯ɴDDDDDDDDDDD2-‘é'"""""""":BÔäת½EåY„ˆ¨Ddú‰¾û† †Š \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_149_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_149_2.dat new file mode 100644 index 0000000..d52e048 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_149_2.dat @@ -0,0 +1 @@ +xÚíÜ;€@ÀÞÓÀý/gcaGÖBXBç'“-ˆ¼ˆ«²»ouէUQ½dõRVOm¡šTƒÎ*ǫÊ;;šŠŠŠŠŠŠŠŠŠj¨ª廝ŠªEe2PQQQQQQQQQQQɨ¨ä TTTTTTTTTTTTr»3•œÁ¼¢¢¢¢¢¢¢¢¢¢¢’3ØR©ä &ÕßTsþŒ·»ªk¤¢z_e2P=ê ïd™ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_153_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_153_2.dat new file mode 100644 index 0000000..3b06041 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_153_2.dat @@ -0,0 +1,2 @@ +xÚíÜ1 +€0 ÐÝӤ÷¿œ‹8ZP!¿úB¶ÒòèèZuÔÈ賶"û“ìbuºñö™‹ÈÒ*îÎÆ²é)]MFFFFFFFFFFFFö†¬%= #‹‘™ddddddddddddr ot2¹†yFFFFFFFFFFFF&× #“k�‘‘‘‘‘‘‘‘‘‘‘‘É5ÈÈL 2222222222²(YÚ�µ½7ÉÈ"d¦ÙóÞ@H— \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_157_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_157_2.dat new file mode 100644 index 0000000..2baf535 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_157_2.dat @@ -0,0 +1,3 @@ +xÚíÜ1 +€0À>¯¹üÿs6‚M¹�ÎqUH1¤XØ&Uç̘½fÝ/u-çûé'ñíæ.ºû[ÍKGGGGGGGGGG÷H×|NG¨“(ttttttttttú¬NF§ÏÊ;::::::::::}–ŽNŸ¥££££££££££Ózèè$ +�>«“Ñé³…ŽŽŽŽŽŽŽŽn­ üAø#ºî^AG—§“(t¯ì =3{ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_161_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_161_2.dat new file mode 100644 index 0000000..d2df759 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_161_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_165_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_165_2.dat new file mode 100644 index 0000000..2e6cd7c --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_165_2.dat @@ -0,0 +1,2 @@ +xÚíÝ1 +€0 ÐÝӤ÷¿œ‹Šè ?òBÆV�ü¥UG�¼žµ%åùÐêþ¸åÙ×*“+ú_ŽfÊËs MIIIIIIIIIù–2d;£¤l¨4‰()))))))))))eöqJنIDIIIIIIIIIII)۠¤”mPRRRRRRRRRRRÊ6lº””² JJJJJJJJJJJJن}œR¶aQRRRRRRRRRRNeòK?R퐔”͔&åW½3U \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_169_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_169_2.dat new file mode 100644 index 0000000..4052062 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_169_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_173_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_173_2.dat new file mode 100644 index 0000000..0a30ba5 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_173_2.dat @@ -0,0 +1 @@ +xÚíÝ+€@ PÏiº÷¿�ÚfÝtóš:>y &d U߬ÔýÍS´´[íþª»ùé¡Õ]5Z;a¼Û5V۞™´´´´´´´´´´AÚÀï[ZÚ˴Œ–––––––––––––V¦«¡ՃI0ZZZZZZZZZZZZZZ=-­L‚ÑÒÒÒÒÒÒÒÒÒÒÒÒÒêÁhiõ`Œ–––––––––––––VFK«“·´´´´´´´´´´´´ÉÚü?‚ݧÍìhioÒJ0Úà}³¢o \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_177_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_177_2.dat new file mode 100644 index 0000000..d2c52f9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_177_2.dat @@ -0,0 +1,2 @@ +xÚíÝ1 +À EÁ>§Yï¹4’V$Í~± ,CŠ…¼&UóŒÞ÷;OoŠk5b÷Ñϙáâ”óŽG°x9¥Û%&&&&&&&&&&&n$îöõOL|‡Øv#&&&&&&&&&&&&&ÖÝbbݍ˜˜˜˜˜˜˜˜˜˜˜˜˜˜Xw#&ÖÝl7bbbbbbbbbbbbbbÝM"ÖÝl7bbbbbbbbbbbbbbÝMa!&Ö݈‰‰‰‰‰‰‰‰‰‰‰‰3Ä)U *F™>– \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_45_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_45_2.dat new file mode 100644 index 0000000..ad44ff1 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_45_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_49_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_49_2.dat new file mode 100644 index 0000000..6e8edff Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_49_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_53_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_53_2.dat new file mode 100644 index 0000000..682cae2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_53_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_57_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_57_2.dat new file mode 100644 index 0000000..66a5c05 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_57_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_61_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_61_2.dat new file mode 100644 index 0000000..77d3815 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_61_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_65_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_65_2.dat new file mode 100644 index 0000000..caf184a Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_65_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_69_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_69_2.dat new file mode 100644 index 0000000..6a3801b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_69_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_73_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_73_2.dat new file mode 100644 index 0000000..74945b7 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_73_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_77_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_77_2.dat new file mode 100644 index 0000000..903cba4 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_77_2.dat @@ -0,0 +1 @@ +xÚíØ1À н§Áû_®‹CMŠØ>òGót �ѫeó¨+Ž×FWÄZE¼­mÓ&gއF£Ѷh‰ùF+×t/�F£ÙYv�F£�´õ³ïµÜì¥Õjçô[*7òÕa¶ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_81_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_81_2.dat new file mode 100644 index 0000000..17a9ac2 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_81_2.dat @@ -0,0 +1,2 @@ +xÚíØ1 +À0н§Ñû_®KÉҩVi!O\"á‘Aø�ˆ«ò]¯:‚x»‘çÃbWµ½1ÅuȦ&‰Ä_‰µT ΋6œH$‰ÒU^‰Dâ~bׯëb=gˆ³âžûXî­ÛÙ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_85_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_85_2.dat new file mode 100644 index 0000000..72c74ff --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_85_2.dat @@ -0,0 +1,2 @@ +xÚíØ1 +À0Ð=§1÷¿\—B7¡ÖÖOþ$á‘A0$â¬ý8W­ ¦ê­þwjguÞu¿­槊¡R©T*u•÷S֧ލæ•J¥R©ÞTóJ¥R©ÿRö‡ÕêޢN¨浘çVû \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_89_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_89_2.dat new file mode 100644 index 0000000..06c9a4f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_89_2.dat @@ -0,0 +1 @@ +xÚíٱ À0 À>ÓÈû/—&E*câÇQqâëÃŃ ªzf$òš«ÈËrM…ßåô„ß<ŽÈsa#d2™L&’?ïArYŸÉd2™LöG"ë3™L&“{ÊÙëRygw“;ÈúÊ ƒñÚI \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_93_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_93_2.dat new file mode 100644 index 0000000..f520296 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_2/mask_93_2.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_2/mask_97_2.dat b/application/libraries/phpqrcode/cache/mask_2/mask_97_2.dat new file mode 100644 index 0000000..38842b9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_2/mask_97_2.dat @@ -0,0 +1,2 @@ +xÚíÙ1 +À0н§Ñû_®Ki·vH4 ’)ë_%s×_½Õƒd3KŽO1^ša†§LŸá,†$’H"‰$’KzRP—t[I&¯ÿX’ÝÍ9‰$’H"‰$’äIò$y’sI$‘DI$ɓäIò$»I$‘Dɍ%eëÌsò!¹¼=—™‚äLAZ5—œ'å̓IVý“rîn¸»/2oƅ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_113_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_113_3.dat new file mode 100644 index 0000000..023b273 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_113_3.dat @@ -0,0 +1,2 @@ +xÚíÚA +€0 DÑ}N“ÞÿrnDëÂFj2úK‚ÂÈCÂt?W«ÜݲîZ“iêÀ.qÕÁ‹ŒoýP� ô¯ªª%SmjÕÖÂ7Ëަ‹:·©ª¨¢Š*ª¨N©Ê:@¤:݇·ìýÁоº•*€¹Š*ª¨¢Š*ª¨f©ÒW9dª2˜«¨¢Š*ª¨¢Šj–*}•S@ Ê`®¢Š*ª¨¢Šj½ªæ¯৪6ðJlº¾êѿÃ}ÕÕ}Õէ€•T‹¿UßaÈ24hnt \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_117_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_117_3.dat new file mode 100644 index 0000000..79cc04d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_117_3.dat @@ -0,0 +1,4 @@ +xÚíÛ1 +Ã0 Ð]§Qî¹.¡xIÚB$úŒ‡€Á?~!É<ÇQ?ß#–ç¶Ð5µ/tIÝJ8owøié{áßçâ¬}£Sø˜^�ã~‡¢½q猉P„ &L˜0áJá¢î1ÊúϻݢðD×K'¬J¨Ä &L˜0ᝅõÃn +«ê0a„ &Lxgaý°Gƒ°*¡&L˜0aÂ{ +g{衟þ2¨ë‡s¢Ή~˜ðì7\]%r¢Jôö9nZ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_121_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_121_3.dat new file mode 100644 index 0000000..aff5a7b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_121_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_125_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_125_3.dat new file mode 100644 index 0000000..e2febdb --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_125_3.dat @@ -0,0 +1,2 @@ +xÚíÛ1 +Ä Ð~N3Þÿrۄ]·‰BБÝ'�?<Âà/̼V۵»Ýóþü¨ ïҏ”¿ÿ´…<–’0-_±Jä[¥ü?† +wÔÄ^;*Ãyòäɓ'Ož<ù’/9ÏÏɯì9±<áîyÓƜ'Ož¥ùz†Öô#EŠ)R¤H‘Z@ªbÂl-)Ìݿñ<ߧ*©OURŸ"5&•ö›š5ý*iúeÎ J]®+ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_145_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_145_3.dat new file mode 100644 index 0000000..338b7e7 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_145_3.dat @@ -0,0 +1,3 @@ +xÚíÜÁ +‚@Ðý|Íóÿ®�”•:Œ^ñ ³Jy̡¤ªyM§î×j‹×-' +9¤V—SÛý֦K¶©9ëÇe¬µ)PíyUûû¹£wËeÞ-m jԨQ£F�5jԨ�RËi٫ÔF4×_wk}·0+µµjRBR£F�5jԨQ»ƒš¾¦eßOMBJHjԨQ£F�5jwPÓ״ì˪IH I�5jԨQ£FͳÇúš–íÙc éw�5jԨQ£FíµŠ:¡œÿÑzS«Ž*2¤¯UZ_«¾CÒ×*­e_Oí´ïZ¥%dIȯûâ†b \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_149_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_149_3.dat new file mode 100644 index 0000000..30bc5fa --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_149_3.dat @@ -0,0 +1 @@ +xÚíÜA‚0Ð}Oó¹ÿåܠ⦐H頯éô“'¬Z×2{oVÛ|Ι«%µ™ê>‚�yû·R{‡!8îäÂI+JpI|#¶Ýfì–5κ[âP A‚ $H� Á‚Q}ð)Õ&<¹÷E‚§ JQ)J� A‚ $H� Ás5z�þ�¥¨%H� A‚ $H�à4A�^£ÿ5A)*E $H� A‚ $è½ �Þ3ïMHQ‚ $H� A‚ç æü3ާ`ÞP¯©®o¢Ý>X{טÔè+±ÑWb£¿¡àìß`IÑã)5׈%dš \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_153_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_153_3.dat new file mode 100644 index 0000000..89cdec0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_153_3.dat @@ -0,0 +1,2 @@ +xÚíÜA +Â0Ð}Nósÿ˹)7mJ¦õ…,Å}8�µêX=c¿W^G“e’ÝNó}úôéÓÿoý ªµ%uJV/µ°“{¿„þ—ô%¿ä§OŸ>}úôéӧOŸþ¨úê}Ýú§êK~ÉOŸ>}úôéӧOŸ>ýQõÕûº=ô/їü’Ÿ>}úôéӧOŸ>}ú£ê«÷u{èïחü’Ÿ>}úôéӧOŸ>}ú£ê«÷u{蟪/ù%?}úôéӧOŸ>}ú¿ׯ.N4ÿ»჏VMm–¢R³•š¥‘�ø™Òá¸t(1ï…¢| \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_177_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_177_3.dat new file mode 100644 index 0000000..9586979 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_177_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_21_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_21_3.dat new file mode 100644 index 0000000..bcb4eec Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_21_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_25_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_25_3.dat new file mode 100644 index 0000000..0ffc375 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_25_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_29_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_29_3.dat new file mode 100644 index 0000000..6150ac1 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_29_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_33_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_33_3.dat new file mode 100644 index 0000000..6053b5e Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_33_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_37_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_37_3.dat new file mode 100644 index 0000000..5dea5b9 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_37_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_41_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_41_3.dat new file mode 100644 index 0000000..ca9ddc2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_41_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_45_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_45_3.dat new file mode 100644 index 0000000..3daad97 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_45_3.dat @@ -0,0 +1,2 @@ +xÚí”K +À D÷sšñþ—ëFJ(&™)ô³0d¡øЇ’³FÞg!Œ[8Ò=èÛ&ËiaÂÎD)Åd8&AËլ¡a³áÀÛä 1'™I–I”«׳ž7Ù9ù­ exÀ�“߾È �ÄÒIìû&֝´¯Cçu´ÍJy‚ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_49_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_49_3.dat new file mode 100644 index 0000000..7f6508d Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_49_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_53_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_53_3.dat new file mode 100644 index 0000000..8800bea --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_53_3.dat @@ -0,0 +1,2 @@ +xÚí–K +À0D÷s½ÿåº ­¥‰Ñ! -(.Bp&�|ˆ"-t–&`Æq‘Q-"†ð9Ù_+)Be/H8¾ãD®¼%‘a~šÐ}spKFN˜úöœ¶Ö=,ÂÆ;‡;a^tª4÷–‰Ï\™ÞF™ÎáÂÀÊÎSNé§éâ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_57_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_57_3.dat new file mode 100644 index 0000000..4e1e5da Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_57_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_61_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_61_3.dat new file mode 100644 index 0000000..bf1a3cc --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_61_3.dat @@ -0,0 +1,2 @@ +xÚí–A +À0ï¾fýÿçz‘4-%š•�Ò*Ãâd°Ðpž!ÝyZܫ‰æ‚uä�õ(ù¸~ë=¹&Éۓ´‹)œÌR2â"/ò"ÿ<9çŠFΊ=r§þbòšó"/ò“ƒîÌíçrw¹"2®¯¹ãçÈøùBòèŸ#3-Ø0-K�ÀW \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_65_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_65_3.dat new file mode 100644 index 0000000..8589208 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_65_3.dat @@ -0,0 +1,2 @@ +xÚí—Q +€ Dÿç4»÷¿\?R•Û …Å,û!¨Oв-òNv�®Í1 :Âc¡Zœuï� Ú"U‹MÕF ~jK¨€´…-la‹[^q^³Q\éœ=�…o”-la‹ÿZpUÒÞB¬„äÿ ±@IµÖKµJzɢ|1Í¢ ÷�¹ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_69_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_69_3.dat new file mode 100644 index 0000000..55318a8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_69_3.dat @@ -0,0 +1,2 @@ +xÚí×Á +€0 Ð{¿&ûÿŸó2¤'—®dô l†=,�¹F½îéy;$§쇤”WEË-R„Ѝ³:¢¡%T,O2½×g…"Ä"‹,²Ȣ/DÍyĈ˜¹ôɧ{þO䮳È"‹,:N„vÈEñWNÎ#(&,ö,‘ä•ºŠ®ë]æx؅ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_73_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_73_3.dat new file mode 100644 index 0000000..15be77f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_73_3.dat @@ -0,0 +1,2 @@ +xÚí˜Q +€0 Cÿ{šìþ—ógè;è¥JJ?dԬK„=ÚRaÃsJhTJ6ex�Îkaº�ú¥§\$é‘n¯´IE,-/ÑÓXB¢�è*х=ËâžíÚÒýweeé4™¢�è¸tÿö̒tšLщî«t t”߫œèà¯bž –gF…fž ÖáqºõoÖd¢Ðdn-ð? \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_77_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_77_3.dat new file mode 100644 index 0000000..ec78280 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_77_3.dat @@ -0,0 +1,2 @@ +xÚíØA +€0 Àû¾&ùÿç¼­BŠiÖRaK"té`I@³¶|¶º…fX—ö±Ôâ­yiöåólõE‘:Sza18G¾À‰if˜K*©¤?–:YÜC1쌞졘(ቷ—êöJ*©¤’¾’jÊl*Õí•TRI×KëÌîR^ؙæœÞksûÂ)cÌ)cÌ)³JZø¦¶óí¥äãa· \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_81_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_81_3.dat new file mode 100644 index 0000000..47bc0f7 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_81_3.dat @@ -0,0 +1,2 @@ +xÚíØ1 +À Fá=§‰÷¿\‘,JÑGAúġ‹ýhjÀÌ>ÚÙ#Ê3X™À’ø�:çk÷Թ\ûFM JÝhu�Çú½3¸¨>ƒT­ZõŸÔ{ÇãPSgøP'¹ÉkÔV¸jժU«¶_ۯý¯U«V­º•»=P Îo†O:Wҝ‹ÞäÝjü[ŸWxÒÎmò� Ù5 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_85_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_85_3.dat new file mode 100644 index 0000000..02c4f8c Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_85_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_89_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_89_3.dat new file mode 100644 index 0000000..2b4cb59 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_89_3.dat @@ -0,0 +1,2 @@ +xÚíÙ1€ …ὧ)÷¿œ *.@àUÁü ƒÓÓ|±¶‰âeŵ6ۢ‚‹äw5*÷ô) oôi¨K®‘¢4nk>Áè1}d>œÐ@ 4ÐXYC¾oœò ۡ1<º©A§ �h �F„ûÆt +ß 4Ð@�µ5´—ž1ÁWrÒï>7û«û†Gíµ}}¤¡x7|ÇN‘gÆîÚN \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_93_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_93_3.dat new file mode 100644 index 0000000..b4cc8a9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_3/mask_93_3.dat @@ -0,0 +1,2 @@ +xÚíÙA +€0 DÑ}N“ÞÿrnJɪQš~éB¨Œú06 û£n®aá¸<<¦ׇ‡ôÿe6×M—R™ýCùÅPÈ ¥LÀ±ô̓išØ9MŽ 2È ƒLkŮ½dD»v*ó"a·ÔX†jBd�AdªdèôZöÀTßd�Ad®�qYø0exö÷ö¼Ÿqe§çÊN¯«Œú�©ª&WV“ææQvcô \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_3/mask_97_3.dat b/application/libraries/phpqrcode/cache/mask_3/mask_97_3.dat new file mode 100644 index 0000000..7adc9eb Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_3/mask_97_3.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_101_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_101_4.dat new file mode 100644 index 0000000..1c97dc0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_101_4.dat @@ -0,0 +1,2 @@ +xÚíšA€ Füÿs^1 b¤‹ÆáИª]ÃØÖ4šm+Æ8Ð+˜Ve¬Ä^H­R]–\Íc‡®ŠÛ +·oõWN#¸îXáéØá+lð ž­HE³cp� \à—^.Î9qñW9ø¼ïç"ò…:¸À.ŸæBßÿó÷ùBƒ \àæ0Ìa˜ÃPǨcp� \ÞÍÅïO•’‹N¥qjpGÖô}“ô}¥Ê$.ä˅ЧЊ� \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_105_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_105_4.dat new file mode 100644 index 0000000..0211cdb --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_105_4.dat @@ -0,0 +1,2 @@ +xÚíšK +€0 D÷=Mrÿ˹‘øA …ÎTáe‘EFóL2 #ŽÈÌ鹢…_I!“‹•ßú­£ç딤Ѻ�““Î-իkm¿Oñá]sS T6*ùª'8Á NŸâ$Þî'N¥Z‹Êí^œ¹}rûäÖUë*GŽ9r|ºcÌ[Á£cNâ¬Þ[_=Æ׫5¼^ÍJ¼�£ù¸ 1*ôq›v \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_117_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_117_4.dat new file mode 100644 index 0000000..3867259 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_117_4.dat @@ -0,0 +1,2 @@ +xÚíÚË +à н_£ÿÿs]4Ø¥©Dg¤nò¸à‰2Jj}´ÖÚҾ·RsSWG¶�R¿Œɧ†žÚ)5¬�»ÞE†áämá#ܯ•ã¾Õðk_"ÃzŸ”š3Â\¹råʕ+×r ­L»k|ê/{„¹;Ž'×àÔ¢�Ø/œ#\pÁ\pÁ…>¢�è#ö ç\pÁ\p¡�è#úˆ>âqŽpÁ\pÁÅ.º¾$¿ºÈIqü𽤠dô‘ÑG’R»°_4¦è ò€˜ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_137_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_137_4.dat new file mode 100644 index 0000000..0c09c48 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_137_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_141_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_141_4.dat new file mode 100644 index 0000000..62b03f2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_141_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_145_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_145_4.dat new file mode 100644 index 0000000..33fb211 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_145_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_149_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_149_4.dat new file mode 100644 index 0000000..de99310 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_149_4.dat @@ -0,0 +1,2 @@ +xÚíÜÁ +!н_£ÿÿsmœÉ Œ„Á+XӋ9=ó.ªõ=Zkça”š]Õޒ> K¯jò´oé ¾ªý|¯SªŠSWKùZmË׌ºƒÃjé�¾€Ã\’йçЪ2ï W\qÅW\qÅÕñ®"~·ß× ­jvÚtòê®òªŠv¥_Ùíƒ\qÅW\qÅW\q%g�3Èä ú•}�+®¸âŠ+®¸âŠ+®ä r9ƒœA¿²rÅW\qÅW\qŕœAÎ g�3èWöAû W\qÅW\qÅW—«œÆ]åVµ~v{êDŸ™3ÔȜ!¹ªƒ\éWÇô«¬Â^<ír/ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_153_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_153_4.dat new file mode 100644 index 0000000..e827dd1 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_153_4.dat @@ -0,0 +1,2 @@ +xÚíØ1ƒ0Àޯñýÿsi,')p!äµ4.ÜГ;±½¿WUmßçj=¿²„²ê°Ê.NO¾þ>úûšçTµڍ[÷¾S7vÜۜg¶q•„?æ÷ÞÒ +š{pe¹o“3Î8ãŒ3Î8ãŒ3Î8{ØYXzðá,°²Û_úO¤ÓYfeñÎô3sÓÜäŒ3Î8ãŒ3Î8ãŒ3¹†\C®!×ÐÏÌMs“3Î8ãŒ3Î8ãŒ3Îär ¹†\C?379ãŒ3Î8ãŒ3Î8ãŒ3¹†\C®!×07ÍMÎ8ãŒ3Î8ãŒ3ÎÎq¶þÚé,»²ËmMrs�›k¤Wvœ3ýìÀ~–WÜ €WB \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_157_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_157_4.dat new file mode 100644 index 0000000..ad5fcf6 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_157_4.dat @@ -0,0 +1 @@ +xÚíÜ1Â0Àޯñýÿs4˜"FPà=iRX¢XÉÛ0ç멪€u 4f�tÑÖl}Òm¾øßýžé➭»Sû|²ËюëSÌ û¶ŽÄPÏ5<]öÉrÇwÜqÇwÜq÷£»È^QáéN6ÏûáéZ¸sߙ³æ,wÜqÇwÜqǝ>«Ïê³ú¬ûΜ5g¹ãŽ;î¸ãŽ;îôY}VŸÕgÝwæ,wÜqÇwÜqÇwú¬>«Ïê³æ¬9ËwÜqÇwÜq§Ïê³ú¬>ë¾3gÍYî¸ãŽ;î¸ãî[ww?P3Üå§ÛþòÑƘÝggtŸ퐮¡;÷]ûû.3àw4A¯ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_161_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_161_4.dat new file mode 100644 index 0000000..7604c45 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_161_4.dat @@ -0,0 +1 @@ +xÚíÚA‚@ Ðýœfæþ—s!AL€�Ä_|,ˆ™4òl)iï¯mŒ²ßl­׉0'¼÷ +E¸û…£ ÷–]N\ºêüxÅ#·‡§2Ÿç/ü¼_¬{—7gÛò9쏼ٷÔÀÖ}�óï2‡rÈ!‡•?}-³#T—Õe9ä�C9ä�C9üÀ¡~ùöý²÷6Sʇ겺Ì!‡rÈ!‡rÈ!‡æôËÞۘs�Õe9ä�C9ä�C94ç _öÞƜƒ|¨.«ËrÈ!‡rÈ!‡šsÐ/›s0ç ªËê2‡rÈ!‡rÈá8ì§}D‚ÃÎìçw£ùýr�DXΡ|xÃ|˜ä!à2ð \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_165_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_165_4.dat new file mode 100644 index 0000000..d83d631 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_165_4.dat @@ -0,0 +1,3 @@ +xÚíÚA +1 Ð}OÓÞÿr.ª"Ñ*?úfÑÅÌP‚óLHIï—kŒµΫõZQ¦…8 +Gy÷êqkñ-ž¿ÿÎn5£Œ½Ú+?ÌÒñùù¾Ýâ¿ø|֎÷Òþà·kKn®E¢¬ñŹä’K.¹ä’K.?ï2º£˜.ó£|¼EJ§{å2<Ê:.åKu\ç’K.¹ä’K.¹ä’Ë—úñ¿êǝmu)_ªãê8—\rÉ%—\rÉ%—\šÛЏ;'2·!_ªãê8—\rÉ%—\rÉ%—\šÛЏ;'2·!_ªã\rÉ%—\rÉ%—\rinC?nnÃ9‘¹ ùRç’K.¹ä’K.¹äò;.û²óHqY'ÊݽéþN·F?ÞKô㕢,êR¾üé|™è My*3 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_169_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_169_4.dat new file mode 100644 index 0000000..4aac95c Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_169_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_173_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_173_4.dat new file mode 100644 index 0000000..9df4d86 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_173_4.dat @@ -0,0 +1,2 @@ +xÚíØK +1À}N“ÜÿrnÂa øÃ~ZY!ÎÐJÑt^ï×5Æ(¸/«õÌjk–z[pjµ§_Ø?~v:|jwúՖ_mû£îXzòoõ�–Ó6?nËÞŠ<ö°j³$pË-·ÜrË-·_p[z¾=ÞVíæ–Qí¦3ÝæTéV¿ÕoÍ ÜrË-·ÜrË-·ÜrË-·r09˜L¦ߚÌ ÜrË-·ÜrË-·ÜrË-·r09˜L¦ߚÌ ÜrË-·ÜrË-·ÜrË-·r09˜L¦ߚÌ ÜrË-·ÜrË-·ÜrË-·r09˜L¦ߚÌ ÜrË-·ÜrË-·Ür›æv­æ6¯Ú÷ßý?•,eå`=*K¬6ڭ~û§ý6£à �´*× \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_177_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_177_4.dat new file mode 100644 index 0000000..6437d25 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_177_4.dat @@ -0,0 +1,2 @@ +xÚíØA +!À»¯Ñÿ.1B„éMyð”![ c÷þ\cŒ²ûZ­çV\¹Üq“Š7ßâÝEó‡Oýzg~Åkëø˿jž;pg”Šu²Í[*7Š׽¥ºöÀŠóTpÌ1ÇsÌ1ÇsÌñqÇ!¯ÿ‹㠊w/òr¯ÿå8«âXÇú±~l®à˜cŽ9æ˜cŽ9æ˜cŽ9–»ÉÝänr7¹›~l®0WpÌ1ÇsÌ1ÇsÌ1ÇËÝänr7¹›~l®0WpÌ1ÇsÌ1ÇsÌ1ÇËÝänr7¹›~¬›+8æ˜cŽ9æ˜cŽ9æ˜cŽånr7¹›ÜM+8æ˜cŽ9æ˜cŽ9æ˜ãtÇû§oEǙŸÉN¥Xy¹[�ËÝR+¾‰cýø¯ûqNÑ\,4J \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_21_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_21_4.dat new file mode 100644 index 0000000..e006b67 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_21_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_25_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_25_4.dat new file mode 100644 index 0000000..0c7c44b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_25_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_29_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_29_4.dat new file mode 100644 index 0000000..c28dc20 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_29_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_33_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_33_4.dat new file mode 100644 index 0000000..5834b6f Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_33_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_37_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_37_4.dat new file mode 100644 index 0000000..4bf2e26 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_37_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_41_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_41_4.dat new file mode 100644 index 0000000..b75b7d0 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_41_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_45_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_45_4.dat new file mode 100644 index 0000000..1b921f3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_45_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_49_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_49_4.dat new file mode 100644 index 0000000..e417f94 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_49_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_53_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_53_4.dat new file mode 100644 index 0000000..7e88826 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_53_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_57_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_57_4.dat new file mode 100644 index 0000000..84669c7 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_57_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_61_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_61_4.dat new file mode 100644 index 0000000..d127c3b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_61_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_65_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_65_4.dat new file mode 100644 index 0000000..c24343d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_65_4.dat @@ -0,0 +1,2 @@ +xÚí—K€ D÷œ¦½ÿåt�X£Й.4ÓE1^ÂãÓhv†»OæÍx„ùéW“ð0 ñ°JH݇½­ðz[ÿ^܈�[vâȾ +y�y�‡ZÀkß=`„™êv·cíǃî…<ȃ<üÚCî/)zàܑ×¯× ¡ÈÃÇÎC²Œš"" \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_69_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_69_4.dat new file mode 100644 index 0000000..a73b114 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_69_4.dat @@ -0,0 +1 @@ +xÚí—;À0CwN÷¿\—ˆ~¤¨;C$3°$<)/m­ºß¥žeΥT±‘2  —:¥]ÿ^VV¡%ƲzAïnÈpv…s"y‘y‘y)zöá¤dÄu6½àª½Gú¾ȋ¼ÈËi^šÂ/<ÊtÓrÂrrÚ)9ͤlõrìó‚�.¶‡Ö \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_73_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_73_4.dat new file mode 100644 index 0000000..72f8922 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_73_4.dat @@ -0,0 +1,3 @@ +xÚí˜A +€0 ïyMòÿÏ顴)1Ý-‚lE3š=Ô}¬ˆ(Öô°9ŸTÇdÎÒò–E/eO ZOžîãÛŻKíY;ÛúpS5Ð+‘ÄëNžäIžŠžà| +"©eìvúö+DݓæNs'Oò¤WŽkîäIž¾ð„ì†̞¸¤FúòrÜi9Î&óô«ÿ ‡]?ÃóA \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_77_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_77_4.dat new file mode 100644 index 0000000..993c486 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_77_4.dat @@ -0,0 +1,2 @@ +xÚí˜Ñ +€0Eßý÷ÿ?×ËjFe8¯0êú uØN©0Õ­µ‰lB´†6‡:«h·<½õ–§¥CÜe\,¹ †ìÔܗ~�ø & rd0 »Sz£7z+ô–êoº»ÓÜþëäÝŽVâ�uÊ:¥7z£·Wo¿š ËÎSÖ)ë”Þèmoù3ë O‹�y�;:*h¥Þ>û¿a€ïCe \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_81_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_81_4.dat new file mode 100644 index 0000000..dd65216 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_81_4.dat @@ -0,0 +1,3 @@ +xÚí˜A +€0 ïyMòÿÏy‘XE m7"89ä 2’Ѹ ûQ1ݳÌëˆ+¸x‰x;ÅÓt35DIY÷¶1x\:už³ýº•}î¦e/ ê§Æ#ñø�¢Th< +‰ùùU¤BzÔË<²×ì5ñˆG<’×ä5{�G<â�¢¿®�5Äñ<饫>¯]ž×UÄr�¿xuÐ ” Üû \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_85_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_85_4.dat new file mode 100644 index 0000000..c8d5123 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_85_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_89_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_89_4.dat new file mode 100644 index 0000000..5b9bd7e --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_89_4.dat @@ -0,0 +1,2 @@ +xÚíÙ1 +€0 …ὧIî9«Æ% Í Š‡ VÞðڀf×r÷ÅÖ0}òzì=¼#9ùìéûÎäҕ:§þû~›sØó¹¨1BՁg&ë4pÆgœq.p.»í°&g·ÿÂT0�5Érgú™sgœqÆçrg捯Ïuô3çÎ8ãŒóë�kÿ.Egm²»b*ÐÍ&›7ÔÉÍÎ?íçúð ¯:Þü \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_93_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_93_4.dat new file mode 100644 index 0000000..be7f5e5 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_4/mask_93_4.dat @@ -0,0 +1,2 @@ +xÚíÙK +À „ὧIî¹n$}P‚ŌBù]¸‘Né§È@ÍÎáî%sŒfkҫ¢}Czò�oA}aʽ2½|¤îé~ØÀªD&žl=¥êÀÜç¦�Yœ®•ÁwÜqÇ}³» q\ÜEéY³˜já®K_âÎyçžÁwÜqÇwz$=’É=ƒ;î¸ãŽû_݋ÿ€>ÜõéŸ+ÑpÓÓöH“öÈéÜ9ï¢DÞi \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_4/mask_97_4.dat b/application/libraries/phpqrcode/cache/mask_4/mask_97_4.dat new file mode 100644 index 0000000..5d848ca Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_4/mask_97_4.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_101_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_101_5.dat new file mode 100644 index 0000000..c21869e --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_101_5.dat @@ -0,0 +1,2 @@ +xÚíšÑ +€ Eßýšíÿ®+£%=ÜM¢3Cbvó¸ Ѭ›«ÍNkû¹´gqõkqq{©%ôO¿ÒòüoçžùÈû,i–¨ÅKÖeœeçÁ3[›|‰îiž¶÷‡¾õ˜ã©V¥¥h]` Æ` Æ0¦ʕ�±¸z˴TÔ0Gu×ì/qŒ8F®„1ƒ±�3¶:WÊò>õ˜´#ŽÇȕ0c0öQÆ8»øE=F#Ž‘+a ÆþÄXÖ͞+cV©%9W>ßíQž]Tk©¹“Y�-gL¸÷óq¬DË艋Ï \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_105_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_105_5.dat new file mode 100644 index 0000000..bc8798c Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_105_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_109_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_109_5.dat new file mode 100644 index 0000000..25a3944 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_109_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_113_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_113_5.dat new file mode 100644 index 0000000..25f42b8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_113_5.dat @@ -0,0 +1,9 @@ +xÚí›Á +ƒ0DïùšÝÿÿ¹^Ò6¥I�63[è[EDqÌc˜ÅÑ+•ÏjÃy¤ì81•û\c +·‹7Úc«åªÆõû?uý}DK™4¦}Çï,¥kkûgì--ž3[÷½´U…Ƃy„UX…UXýSV ³:ϫëû՝�î,÷è|æýкSúô¾Š¯⫰ +«° +«° +«Vɫ¢¼*ÑXÒ[á«ø*¾ +«° +«° +«°zŠUò*ßN÷Vø*¾J€UX…UX…ÕSóèXiýÊjTi4f¹õZkU^­Ôè_‡ìê­~ŠU‘çxþ }ծñZ/r \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_117_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_117_5.dat new file mode 100644 index 0000000..f236940 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_117_5.dat @@ -0,0 +1 @@ +xÚí›Á„ Dï|Mçÿn/*{ÁM¦ÕÍ+„˜pÐI_&m�ˆ-äŽèѾžCæõä­òÏ3­2�u­?o-kgB7wc=¯™‘ªU%yíoÝùRæhӯșDo:ֶy¤R£JkQ^a†a†a†ÿ�áqOgÚiJ¯ýç ;úœ�áqOgÚ)ӊãÃø0 Ã0 Ã0 Ãð[¦v×ö>ç�=>ŒãÃ0 Ã0 Ã0 ÃOf˜z˜3Ž»=>ŒãÃ0 Ã0 Ã0ü.†3ÿ÷ïZ£$¯׷8\õp”ÔÃùw4½Çá:ã¨ÖZÀp¤:q¹ÖÄù§X 7 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_121_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_121_5.dat new file mode 100644 index 0000000..9bb5c41 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_121_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_125_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_125_5.dat new file mode 100644 index 0000000..2161c50 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_125_5.dat @@ -0,0 +1,2 @@ +xÚí›A +Ä E÷ž&¹ÿåfc;S´¶$?‹‹”Š؏��Q4ëáYaßhûyJ}9ºg=×Ú=©liŸ´.;nh_ÍÔýïwzý�ží.�÷qôƒCW”¶üÏȧyº× uëÞë’PkÏ;ÌÃ<ÌÃ<ÌÃ<ÌÃ|*óqëùÎü,‡ m“kWæqçNÊl—%ª ŸÇçñy˜‡y˜‡y˜‡y˜‡ù×Ì×^Ïæ2䰅sX|ŸÇçaæaæaæaæ3ϙ9lH‹Ïãóø<ÌÃ<ÌÃ<ÌÃ<̿›÷웁¿Ì[í¢õüên`ì™TíÚûqж8ó¡^—vôy©öŽ« \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_129_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_129_5.dat new file mode 100644 index 0000000..f0c1d65 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_129_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_133_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_133_5.dat new file mode 100644 index 0000000..46be8b0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_133_5.dat @@ -0,0 +1,2 @@ +xÚíÜA +Ã0 DѽOcÝÿr]4%1mCTåÉÆ¯„Ð÷xΜ[DvÌ=Æá{Fúú–E¤Ϗµˆäq½?ݿÜ9ûkeѭù}±'2^ý•cœçš4G”ž�Û:¶3=J£K-šôF0‚Œ`#ÿÍHÒþˆwð'#ß<{Ú~—Z4é :BG舻F0‚Œ`„áGø~„ŽÐ:‚Œ`#Á?ð#t„ŽÐw-Œ`#Á?ð#ü¡#t#ÁF0r·…ïÙ}Qõš}e¤üÝòR‹Ú;ø<ùçCžéV‹}ї‘ìó¢ËÙY-Hž \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_137_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_137_5.dat new file mode 100644 index 0000000..064e7f2 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_137_5.dat @@ -0,0 +1,3 @@ +xÚíÜÑ +ƒ0 …áû>Móþ/·ÇkÅ1mÂøªÈXº³ü=‰–ö¾µÈhýÕÚîs�å×áH"ãk ÇM„:îúê–3qÒOü¿W}ÿ9’Ԗ¬I”‰“ýHž1ÙG;þ- Ï©sìîڶù?Ò[%M +Å v°ƒìü#;£zg^ÝÔ3Õd}Î6‹“Å9ÛÆΨޙ×@7õ”҄ïð¾#gÃv°ƒì`;׳£ީ\ï$ÔÅåŸð¾ÃwälØÁv°ƒì`çvÔ;ލÖ}7Êwøߑ³a;ØÁv°ƒkA­µ”ïð¾#gÃv°ƒì`ç=N2wúxg§WӤ@n?Ûëc}½SQ“:ûZd?+¨ÏÎú9vÆÎzß)£ÉæÝáP \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_141_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_141_5.dat new file mode 100644 index 0000000..60c1a8e Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_141_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_145_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_145_5.dat new file mode 100644 index 0000000..9303c07 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_145_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_149_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_149_5.dat new file mode 100644 index 0000000..4256cef --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_149_5.dat @@ -0,0 +1,3 @@ +xÚíÜ[ +Â0Ðÿ®&³ÿÍùcµ }-s+'Їœ^;AÇxŽêã=¦ÅëQ­Ï_gUݏﵪæãx­ÆÞGûõTí½ëȺV¹ïúœU̪UE®«å¬æõ_IǴ;ÿŽÇT1×öÅóô̠Š©µ +]W 2È ƒ 2Èà� ¶|oß5Øu�óꆬuI¶:(W�K­U躒ƒrPÊAd�Ad�A; vÞ®íÑoíÝ_zNOæÏ{2rPÊA9È ƒ 2È ƒ 2Èà9íÑß}�^O挞Œ”ƒrÐwQd�Ad�Aýn½žŒßMÈA9(d�Ad�A¯^Wó„² ŽäZ…Ý.+•³GŸ^«ÌÿëKêÉÜÑ`׵}Ë`_FÖê÷�k \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_153_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_153_5.dat new file mode 100644 index 0000000..deea09d --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_153_5.dat @@ -0,0 +1,2 @@ +xÚíÜÑ +‚@Ðw¿fæÿ®—¬¨Ь`ïÖÙD"I¶ÁÃe­ê<:aÔu,7ï«öOfÖ۳šuÀëPÍ6Žî~ñís¾z�ÕÁÏûãsÞÏ,jÄլc¯³ëÌV�öZvߨmé° ¸ì—s^uÜH®YðuÆ&›l²É&›l²É&›_±9¤¯±ÚÜê ;]³Ä^Ðjs«O;ì˜ܔ›rSn²É&›l²É&›l²9½ÍyכƒÖèzAÖ ’›rSnÊM6Ùd“M6Ùd“M6§µéރ»÷@/è±$7å¦ܔ›l²É&›l²É&›lzŽzAžC‘›rSnÊM6Ùd“M6Ùd“ÍOlîþ7ᰚUìu¶Nªã֛»F¯Úcú·óÌêÍP³P›—œSÔ,l;HûO§ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_157_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_157_5.dat new file mode 100644 index 0000000..176e2a6 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_157_5.dat @@ -0,0 +1 @@ +xÚíØ1ƒ0DÑާñÞÿriœ˜( r*�¥· „\™Õ~>Cï£*¥ú¬v¹ïsýØ]Ŝ_{W!Çzﶬ/ÏÝÞõ÷ÝÅÕ)½˙»¹Áöä¤�vÿ ›ÏV‘®×6ÜV‘•޻ð¹Ã,f1‹YÌb³˜�Ê Ìn^o¯¿>¿þí\Oï]øÜñ,Ïò,Ïb³˜Å,f1‹YyVž•gåYžåYžåYÌb³˜Å,f1+Ïʳò¬<˳<˳¾�1‹YÌb³˜Ŭ<+Ïʳò,Ïò,Ïb³˜Å,f1‹YyVž•gåYžåYžåYÌb³˜Å,fÿïÝukys77—Èì}ïvçŠ�Éæmbž=¥wÙsw³)ï»tWÖ†Ó:® \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_161_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_161_5.dat new file mode 100644 index 0000000..70d5fb0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_161_5.dat @@ -0,0 +1,2 @@ +xÚíÜI +ƒ@н§éºÿå²ÑDp@ èo|m� ˜¢ßrHk㨤Ñ~c˜½o³^ÝÃJz­װ‚–#5l»¿î¦S_92Ûî ©“[¯}¾ÜÃÈZÊŸ‡ó=œìTê2ìÿƀ×P‘™2[cVì衆ÌC–Yf™å'-‡ÛìXÎ9>üv§Ë~u»“½sK5ì`Êe¹,—å2Ë,³Ì2Ë,³Ì2Ëç-ë—ßÐ//¯/o_q¾}KÇç¾ä²\–Ër™e–Yf™e–Yf™峖õËoè—Ý+òÿ¹/¹,—å²\f™e–Yf™e–Yfٳúe÷Šx¶B.Ëe¹Ì2Ë,³Ì2Ë,³̲g+ôËîñl…\–ËrÙ16Ë,³Ì2Ë,³üì<œv.×r륆Á½޼‚™ýrO5ÌÿŸöÔs_ýZÎɔuËI¹_ÃìÞ*Ÿ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_165_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_165_5.dat new file mode 100644 index 0000000..94af813 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_165_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_169_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_169_5.dat new file mode 100644 index 0000000..921a770 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_169_5.dat @@ -0,0 +1 @@ +xÚíÛÑj„0Ð÷|Íäÿ®/¶niµ¢K¹£'ŠÈÊ.rö:ÁT-m&¶ZÛx9¯yüq§3rÿµOgàv¼OO_zׯ§u`]ÿüȷ¾ßitëا™ãt½Ùñil¦oc¿‚ö1ã3j9Ž%_g|ëԧ�Æ)ûì³Ï>ûì³Ï>ûϰ¿=×teÌ&õþ_ã4¤Þÿ²¿=×t¥UŸÊ}¹/÷å>ûì³Ï>ûì³Ï>ûO´¯Þr½5/u“¹>¹/÷å¾ÜgŸ}öÙgŸ}öÙgÿ®öÕûO®÷½Ûsv®OîË}¹/÷ÙgŸ}öÙgŸ}öٷ–G½o-�w{¬å‘ûr_î{ægŸ}öÙgŸ}öÙg_½¯Þ÷n�µûì³Ïþ=ì×n]˜4Nk§Öβ_Êðÿ­÷«M½¿ž¦×ûû´Ï8ím?ñÿ´SF…·<· \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_173_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_173_5.dat new file mode 100644 index 0000000..f9a6741 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_173_5.dat @@ -0,0 +1,4 @@ +xÚíÛ[ +ƒ0Ðÿ¬&³ÿÍõG1Ðø¢gäD)[°Cz¼±÷eDÖѷц÷=R¾NÏ6²îóÚFÒíJm� ÞÏJq÷Pœ}ëxÞö séîÑ_¾õ}¶éG�ÚF©y;žíú¿‹ +[;ý]™öé¯eÃk[®ÅQbT«m±yË&0� L`Â̄Y?á¨ÇðرÝÚæ¼w؛· ïfý„£ÃcÇÊÕVN�ä9Á½˜À&0� L`˜�ׄZý„¤}0=F=F9AN�ä÷L`˜À&0� L`Bz¬Y²fI�ñß=F9AN�ä&0� L`˜À&0� ž‹´fI�Ñs‘r‚œ 'È L`˜À&0� L`<i͒£ç"å9AN�˜À&0� L`˜ðVÖ­aB¯XÛ"ý„±²ù×,U­m…>Øö± +=Æw˜�óZ¶gBΜPª¶” !8 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_177_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_177_5.dat new file mode 100644 index 0000000..b07c636 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_177_5.dat @@ -0,0 +1,11 @@ +xÚíÝъÂ0Ð÷~ÍÌÿÿܾحˆšŠ ÎuO,"% ñô:$Xui=¹ÕѶ«×ՃŸïô¸g÷Ƹ?Ώq­.úÕSo~æz׉¹õWï:ó™·=Žh1cÜqóøèñþ]ì”Ƕ¼îiÇÖ!÷¼ßçírß8Ɓó˜¬`+XÁ +V°‚ õеÓê»�j›Ïjž;—8ƁóX®�+ä +¹ÂoV°‚¬`+XÁ +V°â?[1µ^ñh-ֳ5Z;§¶©¶ùrmS®�+ä +¹ÂoV°‚¬`+XÁ +V°â;¬°ËZ,µÍYµM¹B®�+ä +V°‚¬`+XÁ +V°‚ö™Z‹eŸ©ڦ}¦r…\!WȬ`+XÁ +V°‚¬`+ì3µKmÓ>S¹B®�+ä +V°‚¬`+XÁ +V°¢cþò㊛Ç{g;¦^Qq5¡ZU»ÆÕݮûŸQÛLã0+*&YD�qÈñéô*6 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_21_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_21_5.dat new file mode 100644 index 0000000..04f97ea Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_21_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_25_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_25_5.dat new file mode 100644 index 0000000..c20b59b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_25_5.dat @@ -0,0 +1,2 @@ +xڝ‘a +@!ƒÿï4îþ— ޳ʢš™ ?,"ÅÔÌ"j½¦¡Ž?n<¶OÕÛäa ¬w, l}rG‹M;ϦÏ9[ží ¤¢_ú±�x|ŸÊÖ=´l4lK¨ýv½ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_29_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_29_5.dat new file mode 100644 index 0000000..217ec1b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_29_5.dat @@ -0,0 +1,2 @@ +xÚՒ] +À ƒß=M¿û_n0W .«Ž=-ÁŸ4¡mÄy×B€ìÓ+² ‰·ÜR×á“ç􈋆$¼­ƃ.=s/,+îB÷7ž󳝍q®zÄ~§q>=GéÙêŽZùyÎ:—ÜDRŸ« \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_33_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_33_5.dat new file mode 100644 index 0000000..726d7fd Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_33_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_37_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_37_5.dat new file mode 100644 index 0000000..6d32ca6 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_37_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_41_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_41_5.dat new file mode 100644 index 0000000..e07c617 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_41_5.dat @@ -0,0 +1,2 @@ +xÚíTA +À »÷5Íÿ?7˜«ÈXMtxҴx©¤ …¸?@‚˜›7@ò¾~"éN$õ‡SÖɰÄ{ø+C³¨ÛA'Êör\PŒpè<ޏ÷-¼ ͺ:S3s¹ԉùۻËީÎz#žóqw™ó› >þ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_45_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_45_5.dat new file mode 100644 index 0000000..5168a17 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_45_5.dat @@ -0,0 +1 @@ +xÚíUA€ »ï5íÿ?ç„�U:N&Z"†”:;4P1=ƒbNvSGÆM1¶Ü˛½n<ëv`q³¤{ìîßMg§ã¶4þå=Gó-T¹‹º?›“Ôß='kíÈu™ۭ�ž“õ>‘ëß'¯®çþÜ(éκŽö Jô{EßѵsÊ]È ,§sqð \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_49_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_49_5.dat new file mode 100644 index 0000000..9f3f3cd Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_49_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_53_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_53_5.dat new file mode 100644 index 0000000..449807b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_53_5.dat @@ -0,0 +1 @@ +xÚíVAÀ »óúÿÏíâ"Ë zYf5Ɛ˜JC ª ˜A;ÄÜáé¾Âlû\˜,†ëdòR.¯ \(åe_Ýú ³æôеaNi5†ª\żŽ†ÿÐÐë‡aLPòò(¯ÐàÒ;�×ý±2שÃãå¯jÈN6O u…+é¯Ñl{y•«6odúá^ ãÚàçC[‡%® „ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_57_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_57_5.dat new file mode 100644 index 0000000..c7dd81f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_57_5.dat @@ -0,0 +1,2 @@ +xÚíVA +À »÷5Éÿ?·‹NÝlªƒZ‹HAbB‰Z0aÓ ìÎMÆÒÃd`1Ýz”'"<Ր1™æ9nvͨ.ãô )�bݻµ~¤;˜Ÿ9ú¾ÕZ#tõBÁ~ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_65_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_65_5.dat new file mode 100644 index 0000000..ecd9380 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_65_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_69_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_69_5.dat new file mode 100644 index 0000000..ead4edc Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_69_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_73_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_73_5.dat new file mode 100644 index 0000000..0000117 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_73_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_77_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_77_5.dat new file mode 100644 index 0000000..1652cdc --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_77_5.dat @@ -0,0 +1 @@ +xÚí˜Q€ Cÿwšõþ—ó#&C`T£±ÃÃ6¼”ƹ—B¶ü(«Þ©9Ü Ù'ֆäÑÖìƒڢÓÀÀzk·å"hõÜêÝv.` »øâücXöB5[ñ(ÅÖF>71Ó/3�ò4ÎØΪ�z÷^'ÑÔÝ[˜Fyglg•®M>•OÅTLÅ4ϔ{÷&3ëWy*Ÿʧb*¦ï`ºÚ<3õ;´‘îÞVo0÷/s—6nÿ�‘§Ï0Íya½ß[û”ªmácE \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_81_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_81_5.dat new file mode 100644 index 0000000..71215e9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_81_5.dat @@ -0,0 +1,3 @@ +xÚí˜Á +À Cï~Móÿ?·Ãtz°U4½ŒÔ"£ }t�šÕÀMX�2|.îɋ¸˙F\œ�žð¸õÑÂ~mõ‘4� ÷±¿ØX�u +ٔ, ÍÌw—:— E†Ƅ>еXÿ�õ̯=_Ö]�g>±>óÂÆzæמ‡/ë)5ךk±k±kùµüZs­¸X‹µX¿¬Y{ܮÑè}ô¶áç~mt¿æîšìÃÏý:S#™µÑ&;U#)ñÚä \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_85_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_85_5.dat new file mode 100644 index 0000000..09cf0e2 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_5/mask_85_5.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_89_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_89_5.dat new file mode 100644 index 0000000..5fff530 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_89_5.dat @@ -0,0 +1,2 @@ +xÚí™á +à „ÿû4Éû¿Üþ¸5eÆÙå®npQ¤´ Gú™cÖÍf‡µðl^^‡;;bŽ5;`¬k¶©¶U͹߮¾ßj`ÍNósÜùùO=Ú\[a6‡žÁ°¶~ÞnLÍD?‹ ±!6ÄÆ°ñuüÈF%w¿*Ȭ²ÌkÎù¦f¢Ÿ77”SĆØbƒÆê�Xodw_ù�ؗmµ¨â†â†ØbClüÈNۙ ck&äîY¯­Voܡ™׷Bע»بœAl6Æ šæJjÜx \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_93_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_93_5.dat new file mode 100644 index 0000000..ec4240b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_93_5.dat @@ -0,0 +1,2 @@ +xÚí™K +Ã0 D÷>�æþ—ë&©»ð¯µ& áÉÆ fP^”ÁŠ8BY5ÊÇs(imœ®¼ÙҮ¤±®=f—µ÷3¨/wۧ§‡E»¬y¯§ŸïYŽQ¦wf‘á[}¯å¨ ²„[»9ï0303ÏefÇÏ̙Ùñ3'3=<òÆK{níæ¼Sg¨3ü›`f`f`æ¿Ìdø™ޝÞè®oiü£¦ÎPgø7Á ÌÀ Ì܇GµjkÞG]í}?V/æë kÒ×Þ÷ÀWi72–Js™vÃ|*"f^ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_5/mask_97_5.dat b/application/libraries/phpqrcode/cache/mask_5/mask_97_5.dat new file mode 100644 index 0000000..509d117 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_5/mask_97_5.dat @@ -0,0 +1 @@ +xÚíšA„ E÷œ¦½ÿåf!3bf5í¯ƼBˆ‘ùé£|#f=<3l�6<›§Ž_+xjÿ©ÁÛ) “ÙݩÃy°“ïÿ¯ ‰J ¹yXiïܻª5¤IzsٞîcëuÄeQ¡¡ °K°K°K·d)â—>,ͼwx®Dƒַ.,;ãÂsÔ%êg,Á,Á,=€¥ëýRÐóὓ¼7u‰ºÄK°K°ôT–D÷†<(n lY’þŸÞhÐùV۹s÷ޕôy¸Œ¥´ýP±§Eñ<¸öq \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_101_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_101_6.dat new file mode 100644 index 0000000..13f97a0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_101_6.dat @@ -0,0 +1,2 @@ +xÚíšk +„0 „ÿç4Éý/·Ën_.‰Ê2#ˆS‹H”6±_“PâÞZ ›Ïf˳_¡îé,ï7؞Ûà+·%×åWÇÂîd„–âÏnå}–“_&«Ok;Œ�×úP¾Çfµ>³t‚yY×¾]Öns °;s·æs‚Ú,º!Lk¬ÁԅcbLŒ‰12cÔX9Ëó1 ÔÀZç#XÐò±En#;ÏsvŒT~L~L±RŒ‰11vsÆ.‹•”³‹1‚ò±¿ò1ù1ù1ÅJ1&ÆÄ؍ÓÙŃó1ù1ù1ÅJ1&ƞÄ«²gËØ KLƪjl¬¬k{�g¼ڞ5óKò1/ëǐù¯~,aŒ»ù‰õc««ö$“Þ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_105_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_105_6.dat new file mode 100644 index 0000000..a58fec7 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_105_6.dat @@ -0,0 +1,3 @@ +xÚíšQ +ƒ@ Dÿsšäþ—k¡®ÙBÒZ»#o)Sd�}�ÌGܷŠå¹lú쯯Ö^‹�žúú³®)G]Sñ4ÝS×ëšÞÞñ¸ó?½#BºZö:Õã+{þñÅ×s§ÁH¨KþNóiIŽÂ!¦«mþòe1 +RWe9!ÿ¡`ö`ï¶ìUy¯ˀ§uë:檞»ëÖåšU=w×-ûo•ԺwB·}cMÞKßÃ÷蹰{°{ä=òyßÃ÷蹰{°{ä=òyßÃ÷蹰{°wSö¤caoìi'Ýföºé½õyO=C•yO=C•y¯›ß[Ÿ÷Äó{S޻œ=—;Ÿ|v4}ϯ2‰¨¸0§ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_109_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_109_6.dat new file mode 100644 index 0000000..be7b474 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_109_6.dat @@ -0,0 +1 @@ +xÚíšAÂ0 ï~�ýÿρDBHÓC½HãV2‡Æ0Ùn¥uoªò£lø쯯²¯˻=ۢs9[ˆŽõl'«ºÝ?™íë7ÏíR"¯ &“§Û2™ÕßÏ:7QqX_•n» §û]£$Ó՚–EIYôÁ*¦³ÆL”üq0 “0 “0ù{LJýäз(s\�ɳwœô¾‰¦X÷-Ó7^öÑIt�„I˜„I˜„Iü$~?‰N¢“0 “0 “0‰ŸÄOâ'ÑIt’g7LÂ$Lþ-“›„¡€Iuzrfr M÷“^ê'}“éÍ÷“•ùÉ(ÉO~RŸé]1Y²ãL�ĞuÒå9ó¡QÍӕ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_113_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_113_6.dat new file mode 100644 index 0000000..397f527 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_113_6.dat @@ -0,0 +1,3 @@ +xÚí›Ñ Eßû5ôÿN£°¤ՉëõÁÃÈbF6¨on´¶¯,m›>·gSÅ9ìñÞRWã½ðˆcŒǕ9Œ&»%‰1ùöÏ_÷Þcx= GR^°š�w-z?Ãêdzõvî=î,¹ê°}ԥ?ǹژ³:Å9œmÓ=—ó=@U”¶�䲉„UX…UXýVe ³ûÕÜÅ~Õby4WúÕéiæ¿:Ýe=ó‰ó¡€ù’õüɼ—Æú$<>¯OÑôvòÊù'ÅûËåCçÑytæaæaæaæa~›ùòõ|êžÔÙ'9li‹Σóè<ÌÃ<ÌÃ<ÌÃ<ÌÃüóìI‘æç°è<:�ÎÃ<ÌÃ<ÌÃ<ÌÃüóٕ�WæDz¸‚y¿:.z=¿ª Œݓʯ œsØVšöE=ll›_k0_#võ°³ί˜mÝ÷›j \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_129_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_129_6.dat new file mode 100644 index 0000000..b4695c3 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_129_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_133_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_133_6.dat new file mode 100644 index 0000000..40911dc Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_133_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_137_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_137_6.dat new file mode 100644 index 0000000..43ccb68 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_137_6.dat @@ -0,0 +1,2 @@ +xÚíœÑƒ Eßû5ðÿ?·e…¥ÕÍÍ^²4f�Håp[1-e)UQÊV¬û]žU™W÷Nî5ù§o“*8|›Œ÷۩ìW¹£�š6žbk?Ý÷ÿ�{‹f¬´„ì|>¾s֪‡ìür6öö¸“6‡«ú°6Érҟ¶=ûüv¸ڲþWy±Ú ¢¶Š-¸Ê' +ìÀìÀΟ²ãÅ;q tQ�E>U–϶öf‘¯•곭ìxñN]TcÑ(s❮7tÝAwðÙ`v`v`v`çkvˆwfŽwtû;]«ùÞ ;躃Ï;°;°;°;çÙ!ޙ;Þaoôå]ºƒî ;øl°;°;°;°÷ | ʷ èºƒîà³ÁìÀìÀìŒì(3}Œìl.­š?«…"މr}äÇ;Ú\}¼S¦Ù-AžœüwÚ<9;úEVœ'§םˆ�ôó¡ð� \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_141_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_141_6.dat new file mode 100644 index 0000000..0340409 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_141_6.dat @@ -0,0 +1,10 @@ +xÚíœa Fÿ÷4íý/§‰c Òêâ¤]òزȐ…¥ª[²¬¤=Éð[E럓ÖÜósî¹m,éšÛfÒn/ß|kj\ä¿òj§?g½ÝþòÈ[qþû(NO˜Zc5ûŠ©SGGP[oM×Vá’ָ´éf¼¥÷vÖL<·ÇW³�ó»—CöaïzÈ6îÝÉUü¼~һ·{`ný•Џõ�ÙÉݻdvVýy~µrÒ·Z¯"çq·Ûk{>g$ÅáXK¶õÈU}m\b÷ja¤GÞx˜Å,f1‹YÌbö¿™�òÙ]½z¬‘­̞½ƒš^¯�.�ë5Ó[?Ö嬜•³r³˜Å,f1‹YÌòY>Ëgù¬œ•³rVÎb³˜Å,f1‹Y>Ëgù,Ÿ•³rVÎúmŒYÌb³˜Å,fù,Ÿå³|VÎÊY9‹YÌb³˜Å,fù,Ÿå³|VÎÊY9+g1‹YÌb³˜ýŠً ½̦M7>2{9zº϶hŸm3Éçûlò|ã9ßxóټ™ä#f#Ÿx•6 ÿ˜³-îvë%…N' \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_161_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_161_6.dat new file mode 100644 index 0000000..ecec68b Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_161_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_165_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_165_6.dat new file mode 100644 index 0000000..d641dfa Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_165_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_169_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_169_6.dat new file mode 100644 index 0000000..ae68972 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_169_6.dat @@ -0,0 +1 @@ +xÚíÝáJÃ0Ðÿ÷i’÷9[ÛJÓɘók{²1Äb˜!gßnhHkS뉭-­V?·¯KI¯»ïôóׁÏÝ1큏ý1Ýû§ƣݎ/½¨·Á˜ŽæÉ`�þê/zû)*º=°Ÿ3ڏì¿g6õöý^k6ÖÓ5�CæÇéêYË<ÉK¨ÍkMùÚã[õy@ÓGµ&_=~¢²Ï>ûì³Ï>ûì³û㵾 +õþõ�'{«„է_õVˊÚx­-âJýËø<ӛܗûr_î³Ï>ûì³Ï>ûì³iûêý;×ûù÷öœr­OîË}¹/÷ÙgŸ}öÙgŸ}öٿ¦}õþ½ë}÷ö¼x­OîË}¹/÷ÙgŸ}öÙgŸ}öٷ—G½o/�{{ìå‘ûr_îûÎÏ>ûì³Ï>ûì³o/�zß^÷öØË#÷å¾Ü÷�Ÿ}öÙgŸ}öٿ†ý£d²ì'Ÿʳµ|ÔQR½ßNSï·Ã3¹’êý³œËÓãÏåYÖú²Ïäڳÿ‰ZÉ'Çms¿EŸǷj›5ø \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_173_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_173_6.dat new file mode 100644 index 0000000..95fa97c --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_173_6.dat @@ -0,0 +1 @@ +xÚíÝaŠÂ0Ðÿ9MrÿËí¢­M SÜ*:a_-²�5hh_¿)©uÛZ֭[éþ®÷¦lïÓoûےsŸ�mKúš�íø›NÇ{òÿãHÌ?x­Ï`l‡#fï9>’ڟ[¯õ�eé·Єώߓ­¡ ï?þ^ìóöm÷ó®­ð*û/KËm¿—ãhËy%ÞËv-nKl¥í»Âè–í¤kK¸L`˜À&&Ìê g5†¯µ•(·gºwxôY¢ܞîÞáa¬žpVcøZ[‰Æ#O=¡ëSN�ä9Á½˜À&0� L`˜�DŽ¥ê ¹ç't­jŒjŒ]�QN�ä9Á½˜À&0� L`˜�Ás–ÌYRcü@�QN�ä9� L`˜À&0� L`‚ç"ÍYRcô\¤œ 'È r˜À&0� L`˜ÀÏE𳤯è¹H9AN�ä&0� L`˜À&ü²¯ 7špÜ6¯`Â|õ²¬õ„hm¸œs–ò¯ ××ëR5Ƭ™³Ƙ½ȉ k\ÌX/²Ï ‘ )÷g9È \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_177_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_177_6.dat new file mode 100644 index 0000000..e9f0476 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_177_6.dat @@ -0,0 +1,14 @@ +xÚíÝÑn„ …á{žÞÿåڤ*4v۴u¦û¡1‰£²ø{<�L­Gi‘Kí¥ ¿ëGUÌíäŠß÷Ç]çmÜ/ó6žÜÛjÿõ?mZ¤müzÌy/úξgÍjÏÿ\ÌՓ—¢<`Å_¶ã>f{Š¿Ð¿³_ñù,¶,K9o 4Žǵôù�7lËñÞniJiggiår<¤-MGÆ +¬À +¬À +¬À +¬xuVÄ÷+zÌÒRøCÌr9‡+Gqç6ÞQWžñb"ø×Qe×ï"ùWLº‚® +è +ß X�X�X�X�X�/ÁŠ|~Åj,ÖnŒÖmu¼MÞæÚۤ+è +º‚®ð ‚X�X�X�X�Xñ¯Ya,–±X¼Í;¼Mº‚® +è +ß X�X�X�X�X�晋ež)oÓӘÞf’|¦ƒ·™–5�²H“û¸늚í¥÷7/D \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_21_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_21_6.dat new file mode 100644 index 0000000..6bd505b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_21_6.dat @@ -0,0 +1 @@ +xڝ�QÀ Cÿ9M{ÿËi]èÒýX‰1¤À Å-½ C!»�ɳDìû7 W ìٜ§Ø&rDñ)~Î]<MÎ ·3(>{ƒïA ¡«ÿíŽa²¼Sý \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_25_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_25_6.dat new file mode 100644 index 0000000..d45083a --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_25_6.dat @@ -0,0 +1 @@ +xڝQAÀ »÷5öÿŸÛ2)êe+Ä(XmÊZtÆ*(õڹ«;�ÃçtJã<峂†_ڤ‡3°oŠÜ½ڴ"̢a²zh}Ñ&qv€µSGÊ֙,ó-÷‡™J›Í4}³™¦oS[âü}w \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_29_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_29_6.dat new file mode 100644 index 0000000..0408e22 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_29_6.dat @@ -0,0 +1,3 @@ +xÚÕRA +À0 ºûšäÿŸÛXc�Ôõ²ÓL(4E”ԈB +8CÖܾ޳nÃM©�+lǝªÃՆOžé1]�&•ڍ¥4UëD-6-�$:6ÊdZá?yæÛlôÝf£?í˜ åŽ8?ž߲±<Ûlôž}ž¹g›göí * \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_33_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_33_6.dat new file mode 100644 index 0000000..8de4ba5 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_33_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_37_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_37_6.dat new file mode 100644 index 0000000..b37ff0a --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_37_6.dat @@ -0,0 +1 @@ +xÚí”AÀ &ýÿçÚT `½ôèj_ݏ`åÃî8¦޹€GÁ1“œ`–B”Ý`;»ù+ØÂì}åÄÌØ&s£ÒçŒ]<§J…QF=mb3 UŒF'ÚØ6¶Ɩá„E âü€¹5õJa÷wZØÖù60¢¬ +ß[¹·ë¶9¡±ý9¶”ì{Á–SšÛ\ݞ8�Õ`k&­Áœos}{â[Rß¾ý[¥+—þwðºÕO‹èd^jW \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_61_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_61_6.dat new file mode 100644 index 0000000..f2d3f10 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_61_6.dat @@ -0,0 +1,2 @@ +xÚí—KÀ D÷s¸ÿåڤµE~݂�ihÌÄ'™ +ÑŽÍÀòNÏ'oVWßéà£jspèÚCzÓþìÑο©¥ÃaÌÏg!«…#WD%~Ü˃Y?±JÛfŒÚåR€E¸¢ŽÁ�Koæͼ™ǘ§ü|Ƀˎ*ÌOÿP7� Šž‡åÛn¾뼽¥™7ósã†`ž½�ïÌÍÖÀõsúåçdôD¾Ÿÿ¹Ÿsé~>ý<ßiÌK'Žl'¸×9¥ûÐ%.ÆîÄ7 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_65_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_65_6.dat new file mode 100644 index 0000000..550fc8f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_65_6.dat @@ -0,0 +1 @@ +xÚíWQÅ ûïiäþ—{KžNLk?�˜e$ŒQ´ikÝìÆÚ4¸çö1{˜á‰ð×`+®!úڮºM Á?� áû “’°È1b°8†ž £·¦.ŒÞ^�ƒwÌÞò¹ìèsn²ÁFjè�5¹…EaQX|‹=wä¼@°2v<òŋŞ»‰–|4w»êê\ÔUXÇBQz+óÊT±ÈTǹÖcøBÓzž/4­ç8Õë,‘»5Ý`¡¥¨»ý¹ȱ üOV$ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_69_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_69_6.dat new file mode 100644 index 0000000..a3e4fa0 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_69_6.dat @@ -0,0 +1 @@ +xÚí˜KÄ @÷œî¹™dThÃðí¦ hLSSEžq eY@<ã¯+Þ*£|ß窮 %‹®‹>¥¡¨®‹¹zÃ*7ë–Åe´¤öÆ6ãQ¶½©S`šÞ.²¯>»sE Ë'¨%@[‘Ž6°@PË0Ãh £aôFÕxtpl²2§Âóý �‡Q-g1N†feo^ßøÑüë†Ñ0F¯dT>N_ÕOö’‘wË‹G�3ìùªu†åœÁ»gˆå �{‘3<Ê[žԺ b?Â'6ï^ºŠ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_73_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_73_6.dat new file mode 100644 index 0000000..ab71b70 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_73_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_77_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_77_6.dat new file mode 100644 index 0000000..ad5a660 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_77_6.dat @@ -0,0 +1 @@ +xÚí˜Ñà EßùøÿŸ۲i‹ÙÅVäaË.š¦±F©§pST›YÖô4q÷ú~´z…«=Ÿä:ö͒ û6îøÍüôm8‘¾:#0Pض�iDy:2Š'§ùZ­s±Š&}Ïôç滜\r‘0\¥Ś•˜Xw¬Â;iP¬äàȔLœ)ÒS¬±£eԕÜ{hˆDu9÷L‘žb�½•©JÞÒS÷ŒSÆ)™’)™ÞgZš{÷þeÜøŸé)ã”qJ¦dúLw+ƒ#Ó3-V0Åշlî�jƒ¹™ýڠ×S-ÕS ê½9=ݯ÷¦5ÁPPïõq1Mõ?ågÓ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_81_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_81_6.dat new file mode 100644 index 0000000..28a6d07 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_81_6.dat @@ -0,0 +1,3 @@ +xÚí™Q„0Dÿ9 Üÿr»ÉÚR³LÕvök‡šÆ`´¤¯0 º;æi6ÜûçÑÚ\|ñí_¿êccÔ1«¢h¾ýuŒ§½ioÞó#2ƒ¬}x*.YûtÃ&¸­± +ְ¶ÀÖq/K‚;3ÌväeÐ̢ȊÒ‚AÛH±ë?`]é5ÒðK¿¡Êü´†w¿!}{Zû߲žW +Œ”yⷾÀž^§_y­¼k±k±–^K¯•תáb-ÖbÝYSڸ'֜NóÈuÃ×õšÕ#M½fõHS¯Q?|]¯IýðA¯ÆÚi™Mû÷‘yíì¤yuWà \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_85_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_85_6.dat new file mode 100644 index 0000000..d5403e4 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_85_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_89_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_89_6.dat new file mode 100644 index 0000000..eeeb5d1 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_89_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_93_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_93_6.dat new file mode 100644 index 0000000..6ff38db Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_6/mask_93_6.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_6/mask_97_6.dat b/application/libraries/phpqrcode/cache/mask_6/mask_97_6.dat new file mode 100644 index 0000000..3a2072e --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_6/mask_97_6.dat @@ -0,0 +1,2 @@ +xÚíšaƒ0…ÿs¸ÿå¶dÅêÒÒFx[²=4ÆH–ùŠoŠj3«4í&§s}¹*ŽÃž— ÷a V¸�cˆ&3žêÜ5‰arWç^aþ«÷LClÉÒzžqì,ÝÌÑ1†xî µ‰SQ»N»ô»]·¢/GiuÄ`&æ ¢�–wƒ%‚,‘%²D–ÈYú"K½t¸¤+åHE'|R2Ï(v1¨öv–¢ÿqiëëŸqd‰,‘%²D–~€¥�è%ػJjï}íͺĺÄgY"Kd‰,ý+Kñö +–]W–të+sF/)ü]¥.zNòÚÝ'`°>�®½1='#–`+bƒ¥l]Z±”¶­¬ýð \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_101_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_101_7.dat new file mode 100644 index 0000000..1f6bc51 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_101_7.dat @@ -0,0 +1 @@ +xÚíšQà Cÿ}rÿËm“Ê`éÚÊfªj�ò�Té#±¥ÔäÞ54'tûí…îf¼aÈЇo°–$±c<ÙËêãÝÓÕñHݠÌh8„lÕÛûþÁëo=½ĄŽŧàHvº©;‹z€¤!ƒ|/‹FŽЍ3cf̌‰S×ʅ±±㍂¼êñ 8©KŽ)4©9 ”Qç1ç1×J3fÆÌØÅ›S+ÅwÖcçõ˜ó˜ó˜k¥3cf캌ùîâ¾zÌyÌy̵Ҍ™±1¦{¨²bL÷¶çÍX™S+ó�ï.to{>ô˜îmO§ÇJ2²û3cÄ<6ãXn0F´ )� \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_105_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_105_7.dat new file mode 100644 index 0000000..6b0cacf --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_105_7.dat @@ -0,0 +1,2 @@ +xÚíšA +Ä0E÷žFï¹a Mҙ˜N¾Px)¸pQ£¼è_è~ž|ñ(b¿�F’„šç$.Ÿ�a¿o“ÝòWÿGNÚPæUǖ·‰Mþ%{‡ëoôHQôÙUñl¯ִ¿¬�L^>+Éçm­“#{°{�eo&Y2sÛoM•)gncO9s›ßZг´3wÇo™„Ú+Âè{ô=f.ìÁìÁz½‡ޣïÑ÷˜¹°{°{è=ôz�¾GßcæÂìÁÞsÙkCQϞpÓíÊ^¶€&Ñ{^£÷¼Nï­ʷUïÕìï ìe5°ç}¯æE‰wGûßn+o \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_109_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_109_7.dat new file mode 100644 index 0000000..9875cbe --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_109_7.dat @@ -0,0 +1,2 @@ +xÚíšA +…0 D÷9MrÿË}µÚ*˜ _x-d�˜ú:Î"îNJ¢Ý-kÅ"Š⨚¢œ•dŒ{›¿ջü¤·ñã¯ó³ÞÊגɬ|'ƒ�ÒÚràQ5+ s�ûŒ¡)Ûc•Õê7úÕ-ÑÕó»1nn햺qÿɔ–ÔÍJtgô¬^îʉwÿ̘Ň-ùƒ?ÿ*š&Mmý@ee5éø^Ñ +c¢”° +«° +«ÀªÒ,¬¶ýªïbï\1¿×3ýjõ4óØT¿ZžfŢ·�»o鯠«è*º +«° +«° +«°:›Uüêt¿*ü¿ú½¹ºŠ®¢«° +«° +«° +«“Xů²0<·BWÑUtVaVaVƒYÕl +ްªÙkýf5éýª_Ö ¿ªÙkýáW5{­«µ€ä–5pnõC¬éªZ:¬Å bv \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_117_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_117_7.dat new file mode 100644 index 0000000..cde78c1 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_117_7.dat @@ -0,0 +1,2 @@ +xÚí›Yà Dÿç4pÿËU•²�ÈêIª>"ù%6æed ‘Ò8rðS ÍNs޵U¯ánëk5¦Xe­øjÑ»ު®u—Ûî¦ô¿ö±VÛXgøê,l`øøîuÝÊÀî÷£!‚h‰XZù\VãlM±ìë|[ͬÝÂ0 Ã0 Ã0 ÿÃí–#hF'ëøc]�²±ž¾¦iéí>ìú�ŽûHÐat†a†a†a~<ÃÔÆzØyÆqkO‡£Ãè0 Ã0 Ã0 ÃðææŒ㪞F‡Ña†a†aþ)†§2Ç˰éfÒáþ%ƒÀz8ùëátO=¼žâ€3ÿ=Ž‚áÖî3œÜ:ìÿc�w‘ +û�V$¢ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_121_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_121_7.dat new file mode 100644 index 0000000..d5d577f --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_121_7.dat @@ -0,0 +1,2 @@ +xÚí›[ +Ä Eÿ³šdÿ›ú°ŒÆi;^az,ø#6ƞ^rÝׂ§h¶ŽôµÈêÐ&Ýì¨çÜ^amýËÀ•YŸ9×_Òغô5C·r–6ë®ðt^ù^ÅWlûÅÐEä˜ôز~ɿ|èM®èmýÁåÛmSò}÷¶Ó( ۰ ۰ ۰}ŽmQ½]ŒZ§Vüq½]¼ÓvËѲ"™M¹1fÙúGÔÛÛ, q½½ÍB·Ñmt¶a¶a¶a¶ÂöÄz»eF²3�»cxI¹—D·Ñmt¶a¶a¶a¶ÿŸmÎnð’ý$º�n£۰ ۰ ۰ ۏa[}ï¦`[yëæÀö–°¾ޖ޻)Ïn<¹æ4ÜKú/éó¼¤O¢»s§l¨nçlÔmŸµÝ/GëÙ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_125_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_125_7.dat new file mode 100644 index 0000000..f9ec088 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_125_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_129_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_129_7.dat new file mode 100644 index 0000000..9bf51d5 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_129_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_133_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_133_7.dat new file mode 100644 index 0000000..b643ffe Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_133_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_137_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_137_7.dat new file mode 100644 index 0000000..11d212b --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_137_7.dat @@ -0,0 +1,5 @@ +xÚíœÝ +à Fïó4úþ/· úc§‹«_ǂ+¸{üSÚK<ŸÚÎo[l +ìÀìÀΟ²Ó +©ý0û¡7։µVÆl;b¶��–ëüè™7æõfM¾S´†î ;è1ìÀìÀìÀìL³C¾<ßQí[+@wÐt‡˜ v`v`v`v&Ù!ß ›ï°7ÚX+@wÐt‡˜ v`v`v`v8 ÊYP΂¢;èºCÌ;°;°;°saG{™Ä;ڻ>vRœ|ÇžåùŽö®�K¾£½ë£ØMÎðHÖ +‚³#Н(S›ËÎòúqÌçd \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_141_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_141_7.dat new file mode 100644 index 0000000..98dffab --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_141_7.dat @@ -0,0 +1 @@ +xÚíœA„ E÷= Üÿr“IëŒTY@¿ÉÄÅ0!µøø|1”²—št¹bG0µæԗѤ…s��¡2Z/îç¿o�a‚ìøÜ\ÿq¬ãzÜÛOnҋMºÏñÞn™šñt„áX"Kÿ—m‘eM}çÚòCpµí�ÄP™L^¾µŒ„S0S0S0S£L ø)ǔ‚ŸژêY߾%žÔbÑý,õS�©l?ÕzC§Ð)tеLÁLÁLÁLI2…Ÿz�ŸŠRX¶h¾£@§Ð)tеLÁLÁLÁLI1…Ÿz‘ŸbϷóŽ�B§Ð)Ö~0S0S0S0Å7´ø)ö|ù†�B§Ð)˜‚)˜‚)˜‚©÷3Ֆ«L%Ÿ tfª¬wM±Ÿ*:~ªhù©»¡Zºç«sn’cª÷$1UTtJgÆ8‹ÌÕOYºE \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_145_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_145_7.dat new file mode 100644 index 0000000..4aa2bac --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_145_7.dat @@ -0,0 +1,2 @@ +xÚíœÑ +à Eßó5úÿ?W6Zµ ¦-›^é±àƒ2‰q§×ÄbG‰Ÿ¢X6(ƉuÍ"“LbÑbÝGuû÷Žμ®··Gkø¨:HwA[¿jšÔm¯øH¢ø¬ݞï3ïŬýîßâOðkQ{l·|ê»èT›†EmÇ JfÙLÊ?ŸÚ2"&Á¬Á¬Á¬Á¬ÁÚ¬)Äk©ÝRf¢—µÙc�ï“Ñ̉ÓåFŠÑ,§“z©¦�=躆®±‡„5Xƒ5Xƒ5Xƒµ7±F¼¶\¼¦p–�ÚUs#躆®±‡„5Xƒ5Xƒ5Xƒµ°F¼¶d¼ÆYö•ܺ†®¡kì!a Ö` Ö` Ö`�o�‰×8ËæÛct ]CרC¬Á¬Á¬ gMå­‚5…[´N¬%éÄk÷h•ñZp®?›¯Iܣ•ϲƒ^n$ˆÑæÞY7A×ÖÆëZP[¶ fȓ0 \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_149_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_149_7.dat new file mode 100644 index 0000000..809f005 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_149_7.dat @@ -0,0 +1 @@ +xÚíÜÑnà …á{? ¼ÿËu“چ­…´�2»ÕG$nˆˆcççÈF¡µKëÉ×Ðb3ª÷ôþ֪t³îûªPc ç̥Ó7[¢?9:ñÕý['éÁë9'¾*Ӗ à�GæÜa°½hÖ_¿¹/«zÁ+Î6XB‡>ê˜2ôqYÀJ…0Êù黏�šBfaƒÄ 1ˆÁª ȇ9c7Gë ùOl©ô,Å^¥ß�Å꽓òÁ3ƒÕòA:Hé Ä 1ˆA bƒÄàÿ0X4œ%٫½û#ÇÔd>¢&Cé ¤ƒÄ 1ˆA bƒ<šA{ôo¿G¯&óLM†ÒA:H1ˆA bƒÄ 1è¿ {ôj2þ› ƒt�bƒÄ 1ˆA &1Xç`¼_ Ö9ÛðÊ`«™®B˜¶G_çlÃ5™:g5™¶ajMæÍLÔÁŠËèƒIý <|r. \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_153_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_153_7.dat new file mode 100644 index 0000000..c1ab276 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_153_7.dat @@ -0,0 +1,2 @@ +xÚíÜQ‚0„á÷= ½ÿ匉B5ˆ +ю惤/�ͲåïØIÙiº^-àî®Zkm|»YDhë9 ›ÎêÃ\{’KÇÆ¶·{Ÿ�y—³Ç3ßv†¶̻œE]u`RÚÇrý"›ß}ÛY ½ëeÂ2{ÛVV8][×.n:+2_ç¶ÊÂBÃ&6±‰Mlb›ØÄ&6ß`3i¿9�Y³³åΌ諡yÙúU}ԟ82f-vږÕ6 �nÒMºI7±‰Mlb›ØÄ&6±ùólæî7ãÎð‚þÕ ¢›t“nÒMlb›ØÄ&6±‰Mlþ2›ÎüÍÙ^Ð/ˆnÒMºI7±‰Mlb›ØÄ&6±é;gxA¾C¡›t“núO‹Mlb›ØÄ&6±y€Íy•ÇfP¥Õ[6Ÿ4ºß̫�Ùk`v^Ð+Ó9Ä ʬO۱ùð}Ï攨›™+ZXíè®=:iWE \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_157_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_157_7.dat new file mode 100644 index 0000000..2db27f6 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_157_7.dat @@ -0,0 +1,2 @@ +xÚíÜM +ƒ0Eáù[M²ÿ͕‚­ü£s¡_„ ž‰ÇÛã ­}F¹†Qks½‡Ì{Ý%¶WqÛ÷×îø.ž­¬Ýþϯ­]Ü8evV½ßbvV}è®_µÜE̻n3W^KÃ\Ÿ`äÖV캽çZl³˜Å,f1‹YÌþ;³af±Ôkž·ި×úiàè“ÁÄzÍôÖËz?urVÎÊY9‹YÌb³˜Å,fù,Ÿå³|VÎÊY9+g1‹YÌb³˜Å,Ÿå³|–ÏÊY9+gý7Æ,f1‹YÌb³|–ÏòY>+g嬜Å,f1‹YÌb³|–ÏòY>+g嬜•³˜Å,f1‹YÌþÈlâùƳi§o˜ý.\¦ÏƝo<úl;9º:Âg[®϶lŸmÁÔ^œI>=gϙ�œ³-yk_TAµ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_161_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_161_7.dat new file mode 100644 index 0000000..35ba8ff --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_161_7.dat @@ -0,0 +1 @@ +xÚíÜю …áûyšòþ/皨E)Ʈ›î1~4áœý93,˵´ «+µØZT=Zeâ¶C»¹ú.�~iߏµ§¯ÊÞÖ&>Œ,–ÿÔï´6eù˜·í•Ö~,láW]ì ®»º2Íêêº�±Ý\Ñþ;׵2j"–±Œe,ËrXŒݵVÓ(íc쵵ZӖ1‘™§8ީí§�‰±/,'ÇØt™.ÓeºŒe,cËXÆ2–±Œå߱,_þŠ|ytõüç£ï|ÎÜ]¦Ët™.cËXÆ2–±Œe,cyËòå/ɗ­ysî‹.ÓeºL—±Œe,cËXÆ2–±lo…|ÙZ{+è2]¦Ëbl,cËXÆ2–±Œe{+äË֊Ø[A—é2]¦ËXÆ2–±Œe,c9�åÌCÐXÎ<ëþÆò’Ÿ/owsP¾œyÖý]¾œyÖ}·VdÙì氹¯�d9J—Ӈì)Ë1õ jI2ë \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_165_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_165_7.dat new file mode 100644 index 0000000..e27fb8e --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_165_7.dat @@ -0,0 +1 @@ +xÚíÝQŽ‚0Ðÿ·š²ÿÍM&A,NĘ̈!—x0é‡&XZׇZ»-Kث[êÞÉeÉj‡½Œëæx,ƒ§¼ú.ï·g}ë`;ÿþþ+k{ËÑ~1ŸŽÿ}á+k{Ëإž™¾SGùWãgî}‡ݬ@Û?_µö8퐾o+·k][·hô”Wü8~·u·ÜMÆgœqÆg|j<õ·ú¶¶Ú*ßq=<¯”Ïü¤æۓR�wƓëñÕøøÌüÄÌéŸÈq9.Çå8ãŒ3Î8ãŒ3Î8ãIÆÕãU�Gÿ?~µsnr\ŽËq9Î8ãŒ3Î8ãŒ3Îx€qõøÇÕ㮁yç979.Çå¸gœqÆgœqÆgÜ=)êq×À¸'EŽËq9.ÇgœqÆgœqÆwOŠzÜ50îI‘ãr\Žû­Î8ãŒ3Î8ãŒ_ÆøVvd}²ÇÞøü¡aõx˯ÇÛ5êñã)¹&ÿ9)�ñÙÞf¼¥çxþ3øYH]ûS^*/ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_169_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_169_7.dat new file mode 100644 index 0000000..ef1a181 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_169_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_173_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_173_7.dat new file mode 100644 index 0000000..3b51371 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_173_7.dat @@ -0,0 +1 @@ +xÚíÝÝjÃ0 Ð{=Mòþ/· RbÖH¬û«´�rj\Ç=ý,ºm·coúZŽ8;»ïmÏ÷½mÛÝë±0b�÷ƒ_ߚt$~Wu5ÛǚNnÜ'?dÕf2¶í�(oÁï�ßGÚ,Løþù÷õ6_{»zÅÑ÷Æ? Ë9úwq9Ç àS!ƌëÛ9Îoë€î2� L`˜�˜pµŒ¬––O»Enï¶v8L˜²v8L¸º U�áY×"ûd}ê K›r‚œ 'È ÖL`˜À&0� L`B'fÕ:ïO8ÛTcTc\kŒr‚œ 'È ÖL`˜À&0� L`BìY²gI�ñ§kŒr‚œ 'È ÖL`˜À&0� L`‚ç"íYRcô\¤œ 'È r˜À&0� L`˜ÀÏEڳ¤Æè¹H9AN�ä&0� L`˜À&ü'úÿ!Ø;êÅq+¶Yõ„l*´ܳ´þ=؀ã9¶jŒ[2ÚÖÿ€ Ms¤Ÿ‡Ԅ–ç­ë*Ø \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_177_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_177_7.dat new file mode 100644 index 0000000..068477c Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_177_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_21_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_21_7.dat new file mode 100644 index 0000000..4f9f138 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_21_7.dat @@ -0,0 +1,4 @@ +xڝ�Q +À0 Bÿ=�Þÿr]š˜-?]Rl�‡ä݃2•¦¹nc +ð[޹öÝnù�AÃ".–j+êi +~‹x3<úaXÚ{H†ÖC1xÉÀ)â„á¤üSå \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_25_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_25_7.dat new file mode 100644 index 0000000..cefe1b9 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_25_7.dat @@ -0,0 +1 @@ +xڝQAÀ »÷5ôÿŸ[F@©àaƒ„ÄÔÖZ̲… �>;’ÐÀQ±ÊIÖÀQH8Rÿ¼ñ҈GÉ"èz,ù½&‡ñþ;±­Ð'oñ»97”%P8%÷6oǽ�;]ÞúNWnÝ[äf7¹ÌÕvÔ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_29_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_29_7.dat new file mode 100644 index 0000000..e3d7391 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_29_7.dat @@ -0,0 +1,2 @@ +xÚÕR9À ÛýšøÿŸ«ŠÈQpX˜$læŲf!I2pg�SãªêªMZj·ºóÌÃÂ"útŠðï¡e0¾¿#ԛ`�_Ë1-±ÞóŽé´câÝÍ÷òha÷~/¡Eh™4¦Åæ"ä˜~Ï \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_37_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_37_7.dat new file mode 100644 index 0000000..87d9a1a Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_37_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_41_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_41_7.dat new file mode 100644 index 0000000..8acec04 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_41_7.dat @@ -0,0 +1 @@ +xÚíTAÀ »÷5ôÿŸ[fDYæ(O’^¬ÅbR3/~t/L"¹Ä7SQQ5…jÔ\S—šiíñÎb£ß#ՆȏÂ×+ǣ¤w#æzõx?Þ㽧A-ëšw•u曑Y7$b.%AËÇ;•w«Rµ÷ñoxG}?ƒ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_45_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_45_7.dat new file mode 100644 index 0000000..dbba31d Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_45_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_49_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_49_7.dat new file mode 100644 index 0000000..be5dce8 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_49_7.dat @@ -0,0 +1 @@ +xÚíVË€0»÷kàÿÎhœC-X.žÆ�<ì¨h6̋ 3À½ð,B �Ԕó,ç@’ϐ€d5$K¸"T|p•%9"¼ypæ,ïâæ¡=<ÆÉm¡•÷˜ԉ!‹«?¸¦÷DÀW"˜²(®¸Aó-Ö\- J\©{0p¥lÁWW]ƒÒŒ4ºž?4h= Z_ƒÖd+ýg(æ*ã*�+붵(ÏÇæ \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_53_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_53_7.dat new file mode 100644 index 0000000..7028ef6 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_53_7.dat @@ -0,0 +1 @@ +xÚí–KÀ D÷s¼ÿåš&´Råkºiª&,H)øtD×jÁ =¨µÐÎQa˜ž«P"d¹s1ZyC®|hey ­üCvéaH°›7ø‰+ùO…Åu�¥QÎsZtb…°Íð7 µvãúÀŠ­h™VµÌ µ~èùà·1#ÿ~‡[Ëf˜†ùÙæfHkZ¶JtµœŸmZÎÏ6½—ÉsÕúá‹ ƒw¸rÍ&C×߶f \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_57_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_57_7.dat new file mode 100644 index 0000000..ee3107a --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_57_7.dat @@ -0,0 +1 @@ +xÚí—A€0ïûšöÿŸ3&•ÒH)Kb¼Ð&ˆqÅ֞Õ[-ÌÀÞÏ֌ …Ú9Ér¡�Ys ×.«ÎiÝӾ`´ìòØú~—­ëG�é{cœiƒՂ»\Y<˜.©|·Åì2´Ø۟Ù2š ŠQQ[[÷^N·ë€88½líÏÃÁ[}[šPl?g+o*ϖ˜4W¶û�1  ü Õ33˜Ò[¿\Gosó­bkèm”mËômÿÊ^¬¢hë \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_61_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_61_7.dat new file mode 100644 index 0000000..76f8d72 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_61_7.dat @@ -0,0 +1,2 @@ +xÚí—K +À D÷sšäþ—+-µ*5ÿm²¤cŸaT¢1Ø9—�YÌ쌧êL9 糶¬²çísù//i‡‡Ê\„éb®ö©æÂīâÞë-"^²DŒH-iÝ;bnA¢¼™7ófîet¶7ٷỵE:r”<4ß6ó(üu÷y{K3oæ"óÌý|a½�oÌ?ᜟ‡ï竟“ò´pù9åýœj~NêƛÈìs�¹ÑçTYú¡ Âu \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_65_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_65_7.dat new file mode 100644 index 0000000..d8b9206 --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_65_7.dat @@ -0,0 +1 @@ +xÚí—A€ ïûøÿçŒI‘*í6^L!酰¶ ¸i)mTÇT] VW\ï9�e@—4Ku^Â#N%:ö,ÖÙmYÈÂJNã„è9¯“ŠඩˆvAè2úþÑϜ”HÉ"YüšÅÜö +?`ýBXDüBX̽{¿‹Á.³Ëw‘ÿ¨d‘,~΂k’,¸^ïbQâ~±.Ãà\¯wó ®×ë^«ρ÷îOY˜ÞEôJmXâÑÏ"% \ No newline at end of file diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_69_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_69_7.dat new file mode 100644 index 0000000..c2db020 Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_69_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_73_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_73_7.dat new file mode 100644 index 0000000..f414e4a Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_73_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_77_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_77_7.dat new file mode 100644 index 0000000..3e52bfd Binary files /dev/null and b/application/libraries/phpqrcode/cache/mask_7/mask_77_7.dat differ diff --git a/application/libraries/phpqrcode/cache/mask_7/mask_81_7.dat b/application/libraries/phpqrcode/cache/mask_7/mask_81_7.dat new file mode 100644 index 0000000..78e08df --- /dev/null +++ b/application/libraries/phpqrcode/cache/mask_7/mask_81_7.dat @@ -0,0 +1 @@ +xÚí™ÁÄ Dïó5ðÿ?×lÒ"͊ݕ¹u4áÀa„>ukv oÌ40Ý÷ãT±%9¯‘Ô6²ü÷ªU5‹*s�I{`¹ªÿ™_²>S?ç}(:yâTïlï{G&E\”Ö6¨õ}"A’¤X‹õ XϬ«²³Ç<Âmwx°fÜá‘G”back'); + + // user data + $filename = $PNG_TEMP_DIR.'test'.md5($_REQUEST['data'].'|'.$errorCorrectionLevel.'|'.$matrixPointSize).'.png'; + QRcode::png($_REQUEST['data'], $filename, $errorCorrectionLevel, $matrixPointSize, 2); + + } else { + + //default data + echo 'You can provide data in GET parameter: like that
'; + QRcode::png('PHP QR Code :)', $filename, $errorCorrectionLevel, $matrixPointSize, 2); + + } + + //display generated file + echo '
'; + + //config form + echo '
+ Data:   + ECC:   + Size:   +

'; + + // benchmark + QRtools::timeBenchmark(); + + \ No newline at end of file diff --git a/application/libraries/phpqrcode/phpqrcode.php b/application/libraries/phpqrcode/phpqrcode.php new file mode 100644 index 0000000..80adb9d --- /dev/null +++ b/application/libraries/phpqrcode/phpqrcode.php @@ -0,0 +1,3312 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + + +/* + * Version: 1.1.4 + * Build: 2010100721 + */ + + + +//---- qrconst.php ----------------------------- + + + + + +/* + * PHP QR Code encoder + * + * Common constants + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + // Encoding modes + + define('QR_MODE_NUL', -1); + define('QR_MODE_NUM', 0); + define('QR_MODE_AN', 1); + define('QR_MODE_8', 2); + define('QR_MODE_KANJI', 3); + define('QR_MODE_STRUCTURE', 4); + + // Levels of error correction. + + define('QR_ECLEVEL_L', 0); + define('QR_ECLEVEL_M', 1); + define('QR_ECLEVEL_Q', 2); + define('QR_ECLEVEL_H', 3); + + // Supported output formats + + define('QR_FORMAT_TEXT', 0); + define('QR_FORMAT_PNG', 1); + + class qrstr { + public static function set(&$srctab, $x, $y, $repl, $replLen = false) { + $srctab[$y] = substr_replace($srctab[$y], ($replLen !== false)?substr($repl,0,$replLen):$repl, $x, ($replLen !== false)?$replLen:strlen($repl)); + } + } + + + +//---- merged_config.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Config file, tuned-up for merged verion + */ + + define('QR_CACHEABLE', false); // use cache - more disk reads but less CPU power, masks and format templates are stored there + define('QR_CACHE_DIR', false); // used when QR_CACHEABLE === true + define('QR_LOG_DIR', false); // default error logs dir + + define('QR_FIND_BEST_MASK', true); // if true, estimates best mask (spec. default, but extremally slow; set to false to significant performance boost but (propably) worst quality code + define('QR_FIND_FROM_RANDOM', 2); // if false, checks all masks available, otherwise value tells count of masks need to be checked, mask id are got randomly + define('QR_DEFAULT_MASK', 2); // when QR_FIND_BEST_MASK === false + + define('QR_PNG_MAXIMUM_SIZE', 1024); // maximum allowed png image width (in pixels), tune to make sure GD and PHP can handle such big images + + + + +//---- qrtools.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Toolset, handy and debug utilites. + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRtools { + + //---------------------------------------------------------------------- + public static function binarize($frame) + { + $len = count($frame); + foreach ($frame as &$frameLine) { + + for($i=0; $i<$len; $i++) { + $frameLine[$i] = (ord($frameLine[$i])&1)?'1':'0'; + } + } + + return $frame; + } + + //---------------------------------------------------------------------- + public static function tcpdfBarcodeArray($code, $mode = 'QR,L', $tcPdfVersion = '4.5.037') + { + $barcode_array = array(); + + if (!is_array($mode)) + $mode = explode(',', $mode); + + $eccLevel = 'L'; + + if (count($mode) > 1) { + $eccLevel = $mode[1]; + } + + $qrTab = QRcode::text($code, false, $eccLevel); + $size = count($qrTab); + + $barcode_array['num_rows'] = $size; + $barcode_array['num_cols'] = $size; + $barcode_array['bcode'] = array(); + + foreach ($qrTab as $line) { + $arrAdd = array(); + foreach(str_split($line) as $char) + $arrAdd[] = ($char=='1')?1:0; + $barcode_array['bcode'][] = $arrAdd; + } + + return $barcode_array; + } + + //---------------------------------------------------------------------- + public static function clearCache() + { + self::$frames = array(); + } + + //---------------------------------------------------------------------- + public static function buildCache() + { + QRtools::markTime('before_build_cache'); + + $mask = new QRmask(); + for ($a=1; $a <= QRSPEC_VERSION_MAX; $a++) { + $frame = QRspec::newFrame($a); + if (QR_IMAGE) { + $fileName = QR_CACHE_DIR.'frame_'.$a.'.png'; + QRimage::png(self::binarize($frame), $fileName, 1, 0); + } + + $width = count($frame); + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + for ($maskNo=0; $maskNo<8; $maskNo++) + $mask->makeMaskNo($maskNo, $width, $frame, $bitMask, true); + } + + QRtools::markTime('after_build_cache'); + } + + //---------------------------------------------------------------------- + public static function log($outfile, $err) + { + if (QR_LOG_DIR !== false) { + if ($err != '') { + if ($outfile !== false) { + file_put_contents(QR_LOG_DIR.basename($outfile).'-errors.txt', date('Y-m-d H:i:s').': '.$err, FILE_APPEND); + } else { + file_put_contents(QR_LOG_DIR.'errors.txt', date('Y-m-d H:i:s').': '.$err, FILE_APPEND); + } + } + } + } + + //---------------------------------------------------------------------- + public static function dumpMask($frame) + { + $width = count($frame); + for($y=0;$y<$width;$y++) { + for($x=0;$x<$width;$x++) { + echo ord($frame[$y][$x]).','; + } + } + } + + //---------------------------------------------------------------------- + public static function markTime($markerId) + { + list($usec, $sec) = explode(" ", microtime()); + $time = ((float)$usec + (float)$sec); + + if (!isset($GLOBALS['qr_time_bench'])) + $GLOBALS['qr_time_bench'] = array(); + + $GLOBALS['qr_time_bench'][$markerId] = $time; + } + + //---------------------------------------------------------------------- + public static function timeBenchmark() + { + self::markTime('finish'); + + $lastTime = 0; + $startTime = 0; + $p = 0; + + echo ' + + '; + + foreach($GLOBALS['qr_time_bench'] as $markerId=>$thisTime) { + if ($p > 0) { + echo ''; + } else { + $startTime = $thisTime; + } + + $p++; + $lastTime = $thisTime; + } + + echo ' + + +
BENCHMARK
till '.$markerId.': '.number_format($thisTime-$lastTime, 6).'s
TOTAL: '.number_format($lastTime-$startTime, 6).'s
'; + } + + } + + //########################################################################## + + QRtools::markTime('start'); + + + + +//---- qrspec.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * QR Code specifications + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QRSPEC_VERSION_MAX', 40); + define('QRSPEC_WIDTH_MAX', 177); + + define('QRCAP_WIDTH', 0); + define('QRCAP_WORDS', 1); + define('QRCAP_REMINDER', 2); + define('QRCAP_EC', 3); + + class QRspec { + + public static $capacity = array( + array( 0, 0, 0, array( 0, 0, 0, 0)), + array( 21, 26, 0, array( 7, 10, 13, 17)), // 1 + array( 25, 44, 7, array( 10, 16, 22, 28)), + array( 29, 70, 7, array( 15, 26, 36, 44)), + array( 33, 100, 7, array( 20, 36, 52, 64)), + array( 37, 134, 7, array( 26, 48, 72, 88)), // 5 + array( 41, 172, 7, array( 36, 64, 96, 112)), + array( 45, 196, 0, array( 40, 72, 108, 130)), + array( 49, 242, 0, array( 48, 88, 132, 156)), + array( 53, 292, 0, array( 60, 110, 160, 192)), + array( 57, 346, 0, array( 72, 130, 192, 224)), //10 + array( 61, 404, 0, array( 80, 150, 224, 264)), + array( 65, 466, 0, array( 96, 176, 260, 308)), + array( 69, 532, 0, array( 104, 198, 288, 352)), + array( 73, 581, 3, array( 120, 216, 320, 384)), + array( 77, 655, 3, array( 132, 240, 360, 432)), //15 + array( 81, 733, 3, array( 144, 280, 408, 480)), + array( 85, 815, 3, array( 168, 308, 448, 532)), + array( 89, 901, 3, array( 180, 338, 504, 588)), + array( 93, 991, 3, array( 196, 364, 546, 650)), + array( 97, 1085, 3, array( 224, 416, 600, 700)), //20 + array(101, 1156, 4, array( 224, 442, 644, 750)), + array(105, 1258, 4, array( 252, 476, 690, 816)), + array(109, 1364, 4, array( 270, 504, 750, 900)), + array(113, 1474, 4, array( 300, 560, 810, 960)), + array(117, 1588, 4, array( 312, 588, 870, 1050)), //25 + array(121, 1706, 4, array( 336, 644, 952, 1110)), + array(125, 1828, 4, array( 360, 700, 1020, 1200)), + array(129, 1921, 3, array( 390, 728, 1050, 1260)), + array(133, 2051, 3, array( 420, 784, 1140, 1350)), + array(137, 2185, 3, array( 450, 812, 1200, 1440)), //30 + array(141, 2323, 3, array( 480, 868, 1290, 1530)), + array(145, 2465, 3, array( 510, 924, 1350, 1620)), + array(149, 2611, 3, array( 540, 980, 1440, 1710)), + array(153, 2761, 3, array( 570, 1036, 1530, 1800)), + array(157, 2876, 0, array( 570, 1064, 1590, 1890)), //35 + array(161, 3034, 0, array( 600, 1120, 1680, 1980)), + array(165, 3196, 0, array( 630, 1204, 1770, 2100)), + array(169, 3362, 0, array( 660, 1260, 1860, 2220)), + array(173, 3532, 0, array( 720, 1316, 1950, 2310)), + array(177, 3706, 0, array( 750, 1372, 2040, 2430)) //40 + ); + + //---------------------------------------------------------------------- + public static function getDataLength($version, $level) + { + return self::$capacity[$version][QRCAP_WORDS] - self::$capacity[$version][QRCAP_EC][$level]; + } + + //---------------------------------------------------------------------- + public static function getECCLength($version, $level) + { + return self::$capacity[$version][QRCAP_EC][$level]; + } + + //---------------------------------------------------------------------- + public static function getWidth($version) + { + return self::$capacity[$version][QRCAP_WIDTH]; + } + + //---------------------------------------------------------------------- + public static function getRemainder($version) + { + return self::$capacity[$version][QRCAP_REMINDER]; + } + + //---------------------------------------------------------------------- + public static function getMinimumVersion($size, $level) + { + + for($i=1; $i<= QRSPEC_VERSION_MAX; $i++) { + $words = self::$capacity[$i][QRCAP_WORDS] - self::$capacity[$i][QRCAP_EC][$level]; + if($words >= $size) + return $i; + } + + return -1; + } + + //###################################################################### + + public static $lengthTableBits = array( + array(10, 12, 14), + array( 9, 11, 13), + array( 8, 16, 16), + array( 8, 10, 12) + ); + + //---------------------------------------------------------------------- + public static function lengthIndicator($mode, $version) + { + if ($mode == QR_MODE_STRUCTURE) + return 0; + + if ($version <= 9) { + $l = 0; + } else if ($version <= 26) { + $l = 1; + } else { + $l = 2; + } + + return self::$lengthTableBits[$mode][$l]; + } + + //---------------------------------------------------------------------- + public static function maximumWords($mode, $version) + { + if($mode == QR_MODE_STRUCTURE) + return 3; + + if($version <= 9) { + $l = 0; + } else if($version <= 26) { + $l = 1; + } else { + $l = 2; + } + + $bits = self::$lengthTableBits[$mode][$l]; + $words = (1 << $bits) - 1; + + if($mode == QR_MODE_KANJI) { + $words *= 2; // the number of bytes is required + } + + return $words; + } + + // Error correction code ----------------------------------------------- + // Table of the error correction code (Reed-Solomon block) + // See Table 12-16 (pp.30-36), JIS X0510:2004. + + public static $eccTable = array( + array(array( 0, 0), array( 0, 0), array( 0, 0), array( 0, 0)), + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), // 1 + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), + array(array( 1, 0), array( 1, 0), array( 2, 0), array( 2, 0)), + array(array( 1, 0), array( 2, 0), array( 2, 0), array( 4, 0)), + array(array( 1, 0), array( 2, 0), array( 2, 2), array( 2, 2)), // 5 + array(array( 2, 0), array( 4, 0), array( 4, 0), array( 4, 0)), + array(array( 2, 0), array( 4, 0), array( 2, 4), array( 4, 1)), + array(array( 2, 0), array( 2, 2), array( 4, 2), array( 4, 2)), + array(array( 2, 0), array( 3, 2), array( 4, 4), array( 4, 4)), + array(array( 2, 2), array( 4, 1), array( 6, 2), array( 6, 2)), //10 + array(array( 4, 0), array( 1, 4), array( 4, 4), array( 3, 8)), + array(array( 2, 2), array( 6, 2), array( 4, 6), array( 7, 4)), + array(array( 4, 0), array( 8, 1), array( 8, 4), array(12, 4)), + array(array( 3, 1), array( 4, 5), array(11, 5), array(11, 5)), + array(array( 5, 1), array( 5, 5), array( 5, 7), array(11, 7)), //15 + array(array( 5, 1), array( 7, 3), array(15, 2), array( 3, 13)), + array(array( 1, 5), array(10, 1), array( 1, 15), array( 2, 17)), + array(array( 5, 1), array( 9, 4), array(17, 1), array( 2, 19)), + array(array( 3, 4), array( 3, 11), array(17, 4), array( 9, 16)), + array(array( 3, 5), array( 3, 13), array(15, 5), array(15, 10)), //20 + array(array( 4, 4), array(17, 0), array(17, 6), array(19, 6)), + array(array( 2, 7), array(17, 0), array( 7, 16), array(34, 0)), + array(array( 4, 5), array( 4, 14), array(11, 14), array(16, 14)), + array(array( 6, 4), array( 6, 14), array(11, 16), array(30, 2)), + array(array( 8, 4), array( 8, 13), array( 7, 22), array(22, 13)), //25 + array(array(10, 2), array(19, 4), array(28, 6), array(33, 4)), + array(array( 8, 4), array(22, 3), array( 8, 26), array(12, 28)), + array(array( 3, 10), array( 3, 23), array( 4, 31), array(11, 31)), + array(array( 7, 7), array(21, 7), array( 1, 37), array(19, 26)), + array(array( 5, 10), array(19, 10), array(15, 25), array(23, 25)), //30 + array(array(13, 3), array( 2, 29), array(42, 1), array(23, 28)), + array(array(17, 0), array(10, 23), array(10, 35), array(19, 35)), + array(array(17, 1), array(14, 21), array(29, 19), array(11, 46)), + array(array(13, 6), array(14, 23), array(44, 7), array(59, 1)), + array(array(12, 7), array(12, 26), array(39, 14), array(22, 41)), //35 + array(array( 6, 14), array( 6, 34), array(46, 10), array( 2, 64)), + array(array(17, 4), array(29, 14), array(49, 10), array(24, 46)), + array(array( 4, 18), array(13, 32), array(48, 14), array(42, 32)), + array(array(20, 4), array(40, 7), array(43, 22), array(10, 67)), + array(array(19, 6), array(18, 31), array(34, 34), array(20, 61)),//40 + ); + + //---------------------------------------------------------------------- + // CACHEABLE!!! + + public static function getEccSpec($version, $level, array &$spec) + { + if (count($spec) < 5) { + $spec = array(0,0,0,0,0); + } + + $b1 = self::$eccTable[$version][$level][0]; + $b2 = self::$eccTable[$version][$level][1]; + $data = self::getDataLength($version, $level); + $ecc = self::getECCLength($version, $level); + + if($b2 == 0) { + $spec[0] = $b1; + $spec[1] = (int)($data / $b1); + $spec[2] = (int)($ecc / $b1); + $spec[3] = 0; + $spec[4] = 0; + } else { + $spec[0] = $b1; + $spec[1] = (int)($data / ($b1 + $b2)); + $spec[2] = (int)($ecc / ($b1 + $b2)); + $spec[3] = $b2; + $spec[4] = $spec[1] + 1; + } + } + + // Alignment pattern --------------------------------------------------- + + // Positions of alignment patterns. + // This array includes only the second and the third position of the + // alignment patterns. Rest of them can be calculated from the distance + // between them. + + // See Table 1 in Appendix E (pp.71) of JIS X0510:2004. + + public static $alignmentPattern = array( + array( 0, 0), + array( 0, 0), array(18, 0), array(22, 0), array(26, 0), array(30, 0), // 1- 5 + array(34, 0), array(22, 38), array(24, 42), array(26, 46), array(28, 50), // 6-10 + array(30, 54), array(32, 58), array(34, 62), array(26, 46), array(26, 48), //11-15 + array(26, 50), array(30, 54), array(30, 56), array(30, 58), array(34, 62), //16-20 + array(28, 50), array(26, 50), array(30, 54), array(28, 54), array(32, 58), //21-25 + array(30, 58), array(34, 62), array(26, 50), array(30, 54), array(26, 52), //26-30 + array(30, 56), array(34, 60), array(30, 58), array(34, 62), array(30, 54), //31-35 + array(24, 50), array(28, 54), array(32, 58), array(26, 54), array(30, 58), //35-40 + ); + + + /** -------------------------------------------------------------------- + * Put an alignment marker. + * @param frame + * @param width + * @param ox,oy center coordinate of the pattern + */ + public static function putAlignmentMarker(array &$frame, $ox, $oy) + { + $finder = array( + "\xa1\xa1\xa1\xa1\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa0\xa1\xa0\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa1\xa1\xa1\xa1" + ); + + $yStart = $oy-2; + $xStart = $ox-2; + + for($y=0; $y<5; $y++) { + QRstr::set($frame, $xStart, $yStart+$y, $finder[$y]); + } + } + + //---------------------------------------------------------------------- + public static function putAlignmentPattern($version, &$frame, $width) + { + if($version < 2) + return; + + $d = self::$alignmentPattern[$version][1] - self::$alignmentPattern[$version][0]; + if($d < 0) { + $w = 2; + } else { + $w = (int)(($width - self::$alignmentPattern[$version][0]) / $d + 2); + } + + if($w * $w - 3 == 1) { + $x = self::$alignmentPattern[$version][0]; + $y = self::$alignmentPattern[$version][0]; + self::putAlignmentMarker($frame, $x, $y); + return; + } + + $cx = self::$alignmentPattern[$version][0]; + for($x=1; $x<$w - 1; $x++) { + self::putAlignmentMarker($frame, 6, $cx); + self::putAlignmentMarker($frame, $cx, 6); + $cx += $d; + } + + $cy = self::$alignmentPattern[$version][0]; + for($y=0; $y<$w-1; $y++) { + $cx = self::$alignmentPattern[$version][0]; + for($x=0; $x<$w-1; $x++) { + self::putAlignmentMarker($frame, $cx, $cy); + $cx += $d; + } + $cy += $d; + } + } + + // Version information pattern ----------------------------------------- + + // Version information pattern (BCH coded). + // See Table 1 in Appendix D (pp.68) of JIS X0510:2004. + + // size: [QRSPEC_VERSION_MAX - 6] + + public static $versionPattern = array( + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, + 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, + 0x27541, 0x28c69 + ); + + //---------------------------------------------------------------------- + public static function getVersionPattern($version) + { + if($version < 7 || $version > QRSPEC_VERSION_MAX) + return 0; + + return self::$versionPattern[$version -7]; + } + + // Format information -------------------------------------------------- + // See calcFormatInfo in tests/test_qrspec.c (orginal qrencode c lib) + + public static $formatInfo = array( + array(0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976), + array(0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0), + array(0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed), + array(0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b) + ); + + public static function getFormatInfo($mask, $level) + { + if($mask < 0 || $mask > 7) + return 0; + + if($level < 0 || $level > 3) + return 0; + + return self::$formatInfo[$level][$mask]; + } + + // Frame --------------------------------------------------------------- + // Cache of initial frames. + + public static $frames = array(); + + /** -------------------------------------------------------------------- + * Put a finder pattern. + * @param frame + * @param width + * @param ox,oy upper-left coordinate of the pattern + */ + public static function putFinderPattern(&$frame, $ox, $oy) + { + $finder = array( + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1" + ); + + for($y=0; $y<7; $y++) { + QRstr::set($frame, $ox, $oy+$y, $finder[$y]); + } + } + + //---------------------------------------------------------------------- + public static function createFrame($version) + { + $width = self::$capacity[$version][QRCAP_WIDTH]; + $frameLine = str_repeat ("\0", $width); + $frame = array_fill(0, $width, $frameLine); + + // Finder pattern + self::putFinderPattern($frame, 0, 0); + self::putFinderPattern($frame, $width - 7, 0); + self::putFinderPattern($frame, 0, $width - 7); + + // Separator + $yOffset = $width - 7; + + for($y=0; $y<7; $y++) { + $frame[$y][7] = "\xc0"; + $frame[$y][$width - 8] = "\xc0"; + $frame[$yOffset][7] = "\xc0"; + $yOffset++; + } + + $setPattern = str_repeat("\xc0", 8); + + QRstr::set($frame, 0, 7, $setPattern); + QRstr::set($frame, $width-8, 7, $setPattern); + QRstr::set($frame, 0, $width - 8, $setPattern); + + // Format info + $setPattern = str_repeat("\x84", 9); + QRstr::set($frame, 0, 8, $setPattern); + QRstr::set($frame, $width - 8, 8, $setPattern, 8); + + $yOffset = $width - 8; + + for($y=0; $y<8; $y++,$yOffset++) { + $frame[$y][8] = "\x84"; + $frame[$yOffset][8] = "\x84"; + } + + // Timing pattern + + for($i=1; $i<$width-15; $i++) { + $frame[6][7+$i] = chr(0x90 | ($i & 1)); + $frame[7+$i][6] = chr(0x90 | ($i & 1)); + } + + // Alignment pattern + self::putAlignmentPattern($version, $frame, $width); + + // Version information + if($version >= 7) { + $vinf = self::getVersionPattern($version); + + $v = $vinf; + + for($x=0; $x<6; $x++) { + for($y=0; $y<3; $y++) { + $frame[($width - 11)+$y][$x] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + + $v = $vinf; + for($y=0; $y<6; $y++) { + for($x=0; $x<3; $x++) { + $frame[$y][$x+($width - 11)] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + } + + // and a little bit... + $frame[$width - 8][8] = "\x81"; + + return $frame; + } + + //---------------------------------------------------------------------- + public static function debug($frame, $binary_mode = false) + { + if ($binary_mode) { + + foreach ($frame as &$frameLine) { + $frameLine = join('  ', explode('0', $frameLine)); + $frameLine = join('██', explode('1', $frameLine)); + } + + ?> + +


        '; + echo join("
        ", $frame); + echo '






'; + + } else { + + foreach ($frame as &$frameLine) { + $frameLine = join(' ', explode("\xc0", $frameLine)); + $frameLine = join('▒', explode("\xc1", $frameLine)); + $frameLine = join(' ', explode("\xa0", $frameLine)); + $frameLine = join('▒', explode("\xa1", $frameLine)); + $frameLine = join('◇', explode("\x84", $frameLine)); //format 0 + $frameLine = join('◆', explode("\x85", $frameLine)); //format 1 + $frameLine = join('☢', explode("\x81", $frameLine)); //special bit + $frameLine = join(' ', explode("\x90", $frameLine)); //clock 0 + $frameLine = join('◷', explode("\x91", $frameLine)); //clock 1 + $frameLine = join(' ', explode("\x88", $frameLine)); //version + $frameLine = join('▒', explode("\x89", $frameLine)); //version + $frameLine = join('♦', explode("\x01", $frameLine)); + $frameLine = join('⋅', explode("\0", $frameLine)); + } + + ?> + + "; + echo join("
", $frame); + echo "
"; + + } + } + + //---------------------------------------------------------------------- + public static function serial($frame) + { + return gzcompress(join("\n", $frame), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + return explode("\n", gzuncompress($code)); + } + + //---------------------------------------------------------------------- + public static function newFrame($version) + { + if($version < 1 || $version > QRSPEC_VERSION_MAX) + return null; + + if(!isset(self::$frames[$version])) { + + $fileName = QR_CACHE_DIR.'frame_'.$version.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + self::$frames[$version] = self::unserial(file_get_contents($fileName)); + } else { + self::$frames[$version] = self::createFrame($version); + file_put_contents($fileName, self::serial(self::$frames[$version])); + } + } else { + self::$frames[$version] = self::createFrame($version); + } + } + + if(is_null(self::$frames[$version])) + return null; + + return self::$frames[$version]; + } + + //---------------------------------------------------------------------- + public static function rsBlockNum($spec) { return $spec[0] + $spec[3]; } + public static function rsBlockNum1($spec) { return $spec[0]; } + public static function rsDataCodes1($spec) { return $spec[1]; } + public static function rsEccCodes1($spec) { return $spec[2]; } + public static function rsBlockNum2($spec) { return $spec[3]; } + public static function rsDataCodes2($spec) { return $spec[4]; } + public static function rsEccCodes2($spec) { return $spec[2]; } + public static function rsDataLength($spec) { return ($spec[0] * $spec[1]) + ($spec[3] * $spec[4]); } + public static function rsEccLength($spec) { return ($spec[0] + $spec[3]) * $spec[2]; } + + } + + + +//---- qrimage.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Image output of code using GD2 + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QR_IMAGE', true); + + class QRimage { + + //---------------------------------------------------------------------- + public static function png($frame, $filename = false, $pixelPerPoint = 4, $outerFrame = 4,$saveandprint=FALSE) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/png"); + ImagePng($image); + } else { + if($saveandprint===TRUE){ + ImagePng($image, $filename); + header("Content-type: image/png"); + ImagePng($image); + }else{ + ImagePng($image, $filename); + } + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + public static function jpg($frame, $filename = false, $pixelPerPoint = 8, $outerFrame = 4, $q = 85) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/jpeg"); + ImageJpeg($image, null, $q); + } else { + ImageJpeg($image, $filename, $q); + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + private static function image($frame, $pixelPerPoint = 4, $outerFrame = 4) + { + $h = count($frame); + $w = strlen($frame[0]); + + $imgW = $w + 2*$outerFrame; + $imgH = $h + 2*$outerFrame; + + $base_image =ImageCreate($imgW, $imgH); + + $col[0] = ImageColorAllocate($base_image,255,255,255); + $col[1] = ImageColorAllocate($base_image,0,0,0); + + imagefill($base_image, 0, 0, $col[0]); + + for($y=0; $y<$h; $y++) { + for($x=0; $x<$w; $x++) { + if ($frame[$y][$x] == '1') { + ImageSetPixel($base_image,$x+$outerFrame,$y+$outerFrame,$col[1]); + } + } + } + + $target_image =ImageCreate($imgW * $pixelPerPoint, $imgH * $pixelPerPoint); + ImageCopyResized($target_image, $base_image, 0, 0, 0, 0, $imgW * $pixelPerPoint, $imgH * $pixelPerPoint, $imgW, $imgH); + ImageDestroy($base_image); + + return $target_image; + } + } + + + +//---- qrinput.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Input encoding class + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('STRUCTURE_HEADER_BITS', 20); + define('MAX_STRUCTURED_SYMBOLS', 16); + + class QRinputItem { + + public $mode; + public $size; + public $data; + public $bstream; + + public function __construct($mode, $size, $data, $bstream = null) + { + $setData = array_slice($data, 0, $size); + + if (count($setData) < $size) { + $setData = array_merge($setData, array_fill(0,$size-count($setData),0)); + } + + if(!QRinput::check($mode, $size, $setData)) { + throw new Exception('Error m:'.$mode.',s:'.$size.',d:'.join(',',$setData)); + return null; + } + + $this->mode = $mode; + $this->size = $size; + $this->data = $setData; + $this->bstream = $bstream; + } + + //---------------------------------------------------------------------- + public function encodeModeNum($version) + { + try { + + $words = (int)($this->size / 3); + $bs = new QRbitstream(); + + $val = 0x1; + $bs->appendNum(4, $val); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_NUM, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (ord($this->data[$i*3 ]) - ord('0')) * 100; + $val += (ord($this->data[$i*3+1]) - ord('0')) * 10; + $val += (ord($this->data[$i*3+2]) - ord('0')); + $bs->appendNum(10, $val); + } + + if($this->size - $words * 3 == 1) { + $val = ord($this->data[$words*3]) - ord('0'); + $bs->appendNum(4, $val); + } else if($this->size - $words * 3 == 2) { + $val = (ord($this->data[$words*3 ]) - ord('0')) * 10; + $val += (ord($this->data[$words*3+1]) - ord('0')); + $bs->appendNum(7, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeAn($version) + { + try { + $words = (int)($this->size / 2); + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x02); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_AN, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (int)QRinput::lookAnTable(ord($this->data[$i*2 ])) * 45; + $val += (int)QRinput::lookAnTable(ord($this->data[$i*2+1])); + + $bs->appendNum(11, $val); + } + + if($this->size & 1) { + $val = QRinput::lookAnTable(ord($this->data[$words * 2])); + $bs->appendNum(6, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeMode8($version) + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x4); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_8, $version), $this->size); + + for($i=0; $i<$this->size; $i++) { + $bs->appendNum(8, ord($this->data[$i])); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeKanji($version) + { + try { + + $bs = new QRbitrtream(); + + $bs->appendNum(4, 0x8); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_KANJI, $version), (int)($this->size / 2)); + + for($i=0; $i<$this->size; $i+=2) { + $val = (ord($this->data[$i]) << 8) | ord($this->data[$i+1]); + if($val <= 0x9ffc) { + $val -= 0x8140; + } else { + $val -= 0xc140; + } + + $h = ($val >> 8) * 0xc0; + $val = ($val & 0xff) + $h; + + $bs->appendNum(13, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeStructure() + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x03); + $bs->appendNum(4, ord($this->data[1]) - 1); + $bs->appendNum(4, ord($this->data[0]) - 1); + $bs->appendNum(8, ord($this->data[2])); + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function estimateBitStreamSizeOfEntry($version) + { + $bits = 0; + + if($version == 0) + $version = 1; + + switch($this->mode) { + case QR_MODE_NUM: $bits = QRinput::estimateBitsModeNum($this->size); break; + case QR_MODE_AN: $bits = QRinput::estimateBitsModeAn($this->size); break; + case QR_MODE_8: $bits = QRinput::estimateBitsMode8($this->size); break; + case QR_MODE_KANJI: $bits = QRinput::estimateBitsModeKanji($this->size);break; + case QR_MODE_STRUCTURE: return STRUCTURE_HEADER_BITS; + default: + return 0; + } + + $l = QRspec::lengthIndicator($this->mode, $version); + $m = 1 << $l; + $num = (int)(($this->size + $m - 1) / $m); + + $bits += $num * (4 + $l); + + return $bits; + } + + //---------------------------------------------------------------------- + public function encodeBitStream($version) + { + try { + + unset($this->bstream); + $words = QRspec::maximumWords($this->mode, $version); + + if($this->size > $words) { + + $st1 = new QRinputItem($this->mode, $words, $this->data); + $st2 = new QRinputItem($this->mode, $this->size - $words, array_slice($this->data, $words)); + + $st1->encodeBitStream($version); + $st2->encodeBitStream($version); + + $this->bstream = new QRbitstream(); + $this->bstream->append($st1->bstream); + $this->bstream->append($st2->bstream); + + unset($st1); + unset($st2); + + } else { + + $ret = 0; + + switch($this->mode) { + case QR_MODE_NUM: $ret = $this->encodeModeNum($version); break; + case QR_MODE_AN: $ret = $this->encodeModeAn($version); break; + case QR_MODE_8: $ret = $this->encodeMode8($version); break; + case QR_MODE_KANJI: $ret = $this->encodeModeKanji($version);break; + case QR_MODE_STRUCTURE: $ret = $this->encodeModeStructure(); break; + + default: + break; + } + + if($ret < 0) + return -1; + } + + return $this->bstream->size(); + + } catch (Exception $e) { + return -1; + } + } + }; + + //########################################################################## + + class QRinput { + + public $items; + + private $version; + private $level; + + //---------------------------------------------------------------------- + public function __construct($version = 0, $level = QR_ECLEVEL_L) + { + if ($version < 0 || $version > QRSPEC_VERSION_MAX || $level > QR_ECLEVEL_H) { + throw new Exception('Invalid version no'); + return NULL; + } + + $this->version = $version; + $this->level = $level; + } + + //---------------------------------------------------------------------- + public function getVersion() + { + return $this->version; + } + + //---------------------------------------------------------------------- + public function setVersion($version) + { + if($version < 0 || $version > QRSPEC_VERSION_MAX) { + throw new Exception('Invalid version no'); + return -1; + } + + $this->version = $version; + + return 0; + } + + //---------------------------------------------------------------------- + public function getErrorCorrectionLevel() + { + return $this->level; + } + + //---------------------------------------------------------------------- + public function setErrorCorrectionLevel($level) + { + if($level > QR_ECLEVEL_H) { + throw new Exception('Invalid ECLEVEL'); + return -1; + } + + $this->level = $level; + + return 0; + } + + //---------------------------------------------------------------------- + public function appendEntry(QRinputItem $entry) + { + $this->items[] = $entry; + } + + //---------------------------------------------------------------------- + public function append($mode, $size, $data) + { + try { + $entry = new QRinputItem($mode, $size, $data); + $this->items[] = $entry; + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + + public function insertStructuredAppendHeader($size, $index, $parity) + { + if( $size > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong size'); + } + + if( $index <= 0 || $index > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong index'); + } + + $buf = array($size, $index, $parity); + + try { + $entry = new QRinputItem(QR_MODE_STRUCTURE, 3, buf); + array_unshift($this->items, $entry); + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function calcParity() + { + $parity = 0; + + foreach($this->items as $item) { + if($item->mode != QR_MODE_STRUCTURE) { + for($i=$item->size-1; $i>=0; $i--) { + $parity ^= $item->data[$i]; + } + } + } + + return $parity; + } + + //---------------------------------------------------------------------- + public static function checkModeNum($size, $data) + { + for($i=0; $i<$size; $i++) { + if((ord($data[$i]) < ord('0')) || (ord($data[$i]) > ord('9'))){ + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeNum($size) + { + $w = (int)$size / 3; + $bits = $w * 10; + + switch($size - $w * 3) { + case 1: + $bits += 4; + break; + case 2: + $bits += 7; + break; + default: + break; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static $anTable = array( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ); + + //---------------------------------------------------------------------- + public static function lookAnTable($c) + { + return (($c > 127)?-1:self::$anTable[$c]); + } + + //---------------------------------------------------------------------- + public static function checkModeAn($size, $data) + { + for($i=0; $i<$size; $i++) { + if (self::lookAnTable(ord($data[$i])) == -1) { + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeAn($size) + { + $w = (int)($size / 2); + $bits = $w * 11; + + if($size & 1) { + $bits += 6; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static function estimateBitsMode8($size) + { + return $size * 8; + } + + //---------------------------------------------------------------------- + public function estimateBitsModeKanji($size) + { + return (int)(($size / 2) * 13); + } + + //---------------------------------------------------------------------- + public static function checkModeKanji($size, $data) + { + if($size & 1) + return false; + + for($i=0; $i<$size; $i+=2) { + $val = (ord($data[$i]) << 8) | ord($data[$i+1]); + if( $val < 0x8140 + || ($val > 0x9ffc && $val < 0xe040) + || $val > 0xebbf) { + return false; + } + } + + return true; + } + + /*********************************************************************** + * Validation + **********************************************************************/ + + public static function check($mode, $size, $data) + { + if($size <= 0) + return false; + + switch($mode) { + case QR_MODE_NUM: return self::checkModeNum($size, $data); break; + case QR_MODE_AN: return self::checkModeAn($size, $data); break; + case QR_MODE_KANJI: return self::checkModeKanji($size, $data); break; + case QR_MODE_8: return true; break; + case QR_MODE_STRUCTURE: return true; break; + + default: + break; + } + + return false; + } + + + //---------------------------------------------------------------------- + public function estimateBitStreamSize($version) + { + $bits = 0; + + foreach($this->items as $item) { + $bits += $item->estimateBitStreamSizeOfEntry($version); + } + + return $bits; + } + + //---------------------------------------------------------------------- + public function estimateVersion() + { + $version = 0; + $prev = 0; + do { + $prev = $version; + $bits = $this->estimateBitStreamSize($prev); + $version = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if ($version < 0) { + return -1; + } + } while ($version > $prev); + + return $version; + } + + //---------------------------------------------------------------------- + public static function lengthOfCode($mode, $version, $bits) + { + $payload = $bits - 4 - QRspec::lengthIndicator($mode, $version); + switch($mode) { + case QR_MODE_NUM: + $chunks = (int)($payload / 10); + $remain = $payload - $chunks * 10; + $size = $chunks * 3; + if($remain >= 7) { + $size += 2; + } else if($remain >= 4) { + $size += 1; + } + break; + case QR_MODE_AN: + $chunks = (int)($payload / 11); + $remain = $payload - $chunks * 11; + $size = $chunks * 2; + if($remain >= 6) + $size++; + break; + case QR_MODE_8: + $size = (int)($payload / 8); + break; + case QR_MODE_KANJI: + $size = (int)(($payload / 13) * 2); + break; + case QR_MODE_STRUCTURE: + $size = (int)($payload / 8); + break; + default: + $size = 0; + break; + } + + $maxsize = QRspec::maximumWords($mode, $version); + if($size < 0) $size = 0; + if($size > $maxsize) $size = $maxsize; + + return $size; + } + + //---------------------------------------------------------------------- + public function createBitStream() + { + $total = 0; + + foreach($this->items as $item) { + $bits = $item->encodeBitStream($this->version); + + if($bits < 0) + return -1; + + $total += $bits; + } + + return $total; + } + + //---------------------------------------------------------------------- + public function convertData() + { + $ver = $this->estimateVersion(); + if($ver > $this->getVersion()) { + $this->setVersion($ver); + } + + for(;;) { + $bits = $this->createBitStream(); + + if($bits < 0) + return -1; + + $ver = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if($ver < 0) { + throw new Exception('WRONG VERSION'); + return -1; + } else if($ver > $this->getVersion()) { + $this->setVersion($ver); + } else { + break; + } + } + + return 0; + } + + //---------------------------------------------------------------------- + public function appendPaddingBit(&$bstream) + { + $bits = $bstream->size(); + $maxwords = QRspec::getDataLength($this->version, $this->level); + $maxbits = $maxwords * 8; + + if ($maxbits == $bits) { + return 0; + } + + if ($maxbits - $bits < 5) { + return $bstream->appendNum($maxbits - $bits, 0); + } + + $bits += 4; + $words = (int)(($bits + 7) / 8); + + $padding = new QRbitstream(); + $ret = $padding->appendNum($words * 8 - $bits + 4, 0); + + if($ret < 0) + return $ret; + + $padlen = $maxwords - $words; + + if($padlen > 0) { + + $padbuf = array(); + for($i=0; $i<$padlen; $i++) { + $padbuf[$i] = ($i&1)?0x11:0xec; + } + + $ret = $padding->appendBytes($padlen, $padbuf); + + if($ret < 0) + return $ret; + + } + + $ret = $bstream->append($padding); + + return $ret; + } + + //---------------------------------------------------------------------- + public function mergeBitStream() + { + if($this->convertData() < 0) { + return null; + } + + $bstream = new QRbitstream(); + + foreach($this->items as $item) { + $ret = $bstream->append($item->bstream); + if($ret < 0) { + return null; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getBitStream() + { + + $bstream = $this->mergeBitStream(); + + if($bstream == null) { + return null; + } + + $ret = $this->appendPaddingBit($bstream); + if($ret < 0) { + return null; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getByteStream() + { + $bstream = $this->getBitStream(); + if($bstream == null) { + return null; + } + + return $bstream->toByte(); + } + } + + + + + + +//---- qrbitstream.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Bitstream class + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRbitstream { + + public $data = array(); + + //---------------------------------------------------------------------- + public function size() + { + return count($this->data); + } + + //---------------------------------------------------------------------- + public function allocate($setLength) + { + $this->data = array_fill(0, $setLength, 0); + return 0; + } + + //---------------------------------------------------------------------- + public static function newFromNum($bits, $num) + { + $bstream = new QRbitstream(); + $bstream->allocate($bits); + + $mask = 1 << ($bits - 1); + for($i=0; $i<$bits; $i++) { + if($num & $mask) { + $bstream->data[$i] = 1; + } else { + $bstream->data[$i] = 0; + } + $mask = $mask >> 1; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public static function newFromBytes($size, $data) + { + $bstream = new QRbitstream(); + $bstream->allocate($size * 8); + $p=0; + + for($i=0; $i<$size; $i++) { + $mask = 0x80; + for($j=0; $j<8; $j++) { + if($data[$i] & $mask) { + $bstream->data[$p] = 1; + } else { + $bstream->data[$p] = 0; + } + $p++; + $mask = $mask >> 1; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function append(QRbitstream $arg) + { + if (is_null($arg)) { + return -1; + } + + if($arg->size() == 0) { + return 0; + } + + if($this->size() == 0) { + $this->data = $arg->data; + return 0; + } + + $this->data = array_values(array_merge($this->data, $arg->data)); + + return 0; + } + + //---------------------------------------------------------------------- + public function appendNum($bits, $num) + { + if ($bits == 0) + return 0; + + $b = QRbitstream::newFromNum($bits, $num); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function appendBytes($size, $data) + { + if ($size == 0) + return 0; + + $b = QRbitstream::newFromBytes($size, $data); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function toByte() + { + + $size = $this->size(); + + if($size == 0) { + return array(); + } + + $data = array_fill(0, (int)(($size + 7) / 8), 0); + $bytes = (int)($size / 8); + + $p = 0; + + for($i=0; $i<$bytes; $i++) { + $v = 0; + for($j=0; $j<8; $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$i] = $v; + } + + if($size & 7) { + $v = 0; + for($j=0; $j<($size & 7); $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$bytes] = $v; + } + + return $data; + } + + } + + + + +//---- qrsplit.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Input splitting classes + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + class QRsplit { + + public $dataStr = ''; + public $input; + public $modeHint; + + //---------------------------------------------------------------------- + public function __construct($dataStr, $input, $modeHint) + { + $this->dataStr = $dataStr; + $this->input = $input; + $this->modeHint = $modeHint; + } + + //---------------------------------------------------------------------- + public static function isdigitat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return ((ord($str[$pos]) >= ord('0'))&&(ord($str[$pos]) <= ord('9'))); + } + + //---------------------------------------------------------------------- + public static function isalnumat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return (QRinput::lookAnTable(ord($str[$pos])) >= 0); + } + + //---------------------------------------------------------------------- + public function identifyMode($pos) + { + if ($pos >= strlen($this->dataStr)) + return QR_MODE_NUL; + + $c = $this->dataStr[$pos]; + + if(self::isdigitat($this->dataStr, $pos)) { + return QR_MODE_NUM; + } else if(self::isalnumat($this->dataStr, $pos)) { + return QR_MODE_AN; + } else if($this->modeHint == QR_MODE_KANJI) { + + if ($pos+1 < strlen($this->dataStr)) + { + $d = $this->dataStr[$pos+1]; + $word = (ord($c) << 8) | ord($d); + if(($word >= 0x8140 && $word <= 0x9ffc) || ($word >= 0xe040 && $word <= 0xebbf)) { + return QR_MODE_KANJI; + } + } + } + + return QR_MODE_8; + } + + //---------------------------------------------------------------------- + public function eatNum() + { + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + while(self::isdigitat($this->dataStr, $p)) { + $p++; + } + + $run = $p; + $mode = $this->identifyMode($p); + + if($mode == QR_MODE_8) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + if($mode == QR_MODE_AN) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsModeAn(1) // + 4 + la + - QRinput::estimateBitsModeAn($run + 1);// - 4 - la + if($dif > 0) { + return $this->eatAn(); + } + } + + $ret = $this->input->append(QR_MODE_NUM, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatAn() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + + while(self::isalnumat($this->dataStr, $p)) { + if(self::isdigitat($this->dataStr, $p)) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + + $dif = QRinput::estimateBitsModeAn($p) // + 4 + la + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsModeAn($q); // - 4 - la + + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + + if(!self::isalnumat($this->dataStr, $p)) { + $dif = QRinput::estimateBitsModeAn($run) + 4 + $la + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + + $ret = $this->input->append(QR_MODE_AN, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatKanji() + { + $p = 0; + + while($this->identifyMode($p) == QR_MODE_KANJI) { + $p += 2; + } + + $ret = $this->input->append(QR_MODE_KANJI, $p, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eat8() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 1; + $dataStrLen = strlen($this->dataStr); + + while($p < $dataStrLen) { + + $mode = $this->identifyMode($p); + if($mode == QR_MODE_KANJI) { + break; + } + if($mode == QR_MODE_NUM) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else if($mode == QR_MODE_AN) { + $q = $p; + while(self::isalnumat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeAn($q - $p) + 4 + $la + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + $ret = $this->input->append(QR_MODE_8, $run, str_split($this->dataStr)); + + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function splitString() + { + while (strlen($this->dataStr) > 0) + { + if($this->dataStr == '') + return 0; + + $mode = $this->identifyMode(0); + + switch ($mode) { + case QR_MODE_NUM: $length = $this->eatNum(); break; + case QR_MODE_AN: $length = $this->eatAn(); break; + case QR_MODE_KANJI: + if ($hint == QR_MODE_KANJI) + $length = $this->eatKanji(); + else $length = $this->eat8(); + break; + default: $length = $this->eat8(); break; + + } + + if($length == 0) return 0; + if($length < 0) return -1; + + $this->dataStr = substr($this->dataStr, $length); + } + } + + //---------------------------------------------------------------------- + public function toUpper() + { + $stringLen = strlen($this->dataStr); + $p = 0; + + while ($p<$stringLen) { + $mode = self::identifyMode(substr($this->dataStr, $p), $this->modeHint); + if($mode == QR_MODE_KANJI) { + $p += 2; + } else { + if (ord($this->dataStr[$p]) >= ord('a') && ord($this->dataStr[$p]) <= ord('z')) { + $this->dataStr[$p] = chr(ord($this->dataStr[$p]) - 32); + } + $p++; + } + } + + return $this->dataStr; + } + + //---------------------------------------------------------------------- + public static function splitStringToQRinput($string, QRinput $input, $modeHint, $casesensitive = true) + { + if(is_null($string) || $string == '\0' || $string == '') { + throw new Exception('empty string!!!'); + } + + $split = new QRsplit($string, $input, $modeHint); + + if(!$casesensitive) + $split->toUpper(); + + return $split->splitString(); + } + } + + + +//---- qrrscode.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Reed-Solomon error correction support + * + * Copyright (C) 2002, 2003, 2004, 2006 Phil Karn, KA9Q + * (libfec is released under the GNU Lesser General Public License.) + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsItem { + + public $mm; // Bits per symbol + public $nn; // Symbols per block (= (1<= $this->nn) { + $x -= $this->nn; + $x = ($x >> $this->mm) + ($x & $this->nn); + } + + return $x; + } + + //---------------------------------------------------------------------- + public static function init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + // Common code for intializing a Reed-Solomon control block (char or int symbols) + // Copyright 2004 Phil Karn, KA9Q + // May be used under the terms of the GNU Lesser General Public License (LGPL) + + $rs = null; + + // Check parameter ranges + if($symsize < 0 || $symsize > 8) return $rs; + if($fcr < 0 || $fcr >= (1<<$symsize)) return $rs; + if($prim <= 0 || $prim >= (1<<$symsize)) return $rs; + if($nroots < 0 || $nroots >= (1<<$symsize)) return $rs; // Can't have more roots than symbol values! + if($pad < 0 || $pad >= ((1<<$symsize) -1 - $nroots)) return $rs; // Too much padding + + $rs = new QRrsItem(); + $rs->mm = $symsize; + $rs->nn = (1<<$symsize)-1; + $rs->pad = $pad; + + $rs->alpha_to = array_fill(0, $rs->nn+1, 0); + $rs->index_of = array_fill(0, $rs->nn+1, 0); + + // PHP style macro replacement ;) + $NN =& $rs->nn; + $A0 =& $NN; + + // Generate Galois field lookup tables + $rs->index_of[0] = $A0; // log(zero) = -inf + $rs->alpha_to[$A0] = 0; // alpha**-inf = 0 + $sr = 1; + + for($i=0; $i<$rs->nn; $i++) { + $rs->index_of[$sr] = $i; + $rs->alpha_to[$i] = $sr; + $sr <<= 1; + if($sr & (1<<$symsize)) { + $sr ^= $gfpoly; + } + $sr &= $rs->nn; + } + + if($sr != 1){ + // field generator polynomial is not primitive! + $rs = NULL; + return $rs; + } + + /* Form RS code generator polynomial from its roots */ + $rs->genpoly = array_fill(0, $nroots+1, 0); + + $rs->fcr = $fcr; + $rs->prim = $prim; + $rs->nroots = $nroots; + $rs->gfpoly = $gfpoly; + + /* Find prim-th root of 1, used in decoding */ + for($iprim=1;($iprim % $prim) != 0;$iprim += $rs->nn) + ; // intentional empty-body loop! + + $rs->iprim = (int)($iprim / $prim); + $rs->genpoly[0] = 1; + + for ($i = 0,$root=$fcr*$prim; $i < $nroots; $i++, $root += $prim) { + $rs->genpoly[$i+1] = 1; + + // Multiply rs->genpoly[] by @**(root + x) + for ($j = $i; $j > 0; $j--) { + if ($rs->genpoly[$j] != 0) { + $rs->genpoly[$j] = $rs->genpoly[$j-1] ^ $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[$j]] + $root)]; + } else { + $rs->genpoly[$j] = $rs->genpoly[$j-1]; + } + } + // rs->genpoly[0] can never be zero + $rs->genpoly[0] = $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[0]] + $root)]; + } + + // convert rs->genpoly[] to index form for quicker encoding + for ($i = 0; $i <= $nroots; $i++) + $rs->genpoly[$i] = $rs->index_of[$rs->genpoly[$i]]; + + return $rs; + } + + //---------------------------------------------------------------------- + public function encode_rs_char($data, &$parity) + { + $MM =& $this->mm; + $NN =& $this->nn; + $ALPHA_TO =& $this->alpha_to; + $INDEX_OF =& $this->index_of; + $GENPOLY =& $this->genpoly; + $NROOTS =& $this->nroots; + $FCR =& $this->fcr; + $PRIM =& $this->prim; + $IPRIM =& $this->iprim; + $PAD =& $this->pad; + $A0 =& $NN; + + $parity = array_fill(0, $NROOTS, 0); + + for($i=0; $i< ($NN-$NROOTS-$PAD); $i++) { + + $feedback = $INDEX_OF[$data[$i] ^ $parity[0]]; + if($feedback != $A0) { + // feedback term is non-zero + + // This line is unnecessary when GENPOLY[NROOTS] is unity, as it must + // always be for the polynomials constructed by init_rs() + $feedback = $this->modnn($NN - $GENPOLY[$NROOTS] + $feedback); + + for($j=1;$j<$NROOTS;$j++) { + $parity[$j] ^= $ALPHA_TO[$this->modnn($feedback + $GENPOLY[$NROOTS-$j])]; + } + } + + // Shift + array_shift($parity); + if($feedback != $A0) { + array_push($parity, $ALPHA_TO[$this->modnn($feedback + $GENPOLY[0])]); + } else { + array_push($parity, 0); + } + } + } + } + + //########################################################################## + + class QRrs { + + public static $items = array(); + + //---------------------------------------------------------------------- + public static function init_rs($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + foreach(self::$items as $rs) { + if($rs->pad != $pad) continue; + if($rs->nroots != $nroots) continue; + if($rs->mm != $symsize) continue; + if($rs->gfpoly != $gfpoly) continue; + if($rs->fcr != $fcr) continue; + if($rs->prim != $prim) continue; + + return $rs; + } + + $rs = QRrsItem::init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad); + array_unshift(self::$items, $rs); + + return $rs; + } + } + + + +//---- qrmask.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Masking + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('N1', 3); + define('N2', 3); + define('N3', 40); + define('N4', 10); + + class QRmask { + + public $runLength = array(); + + //---------------------------------------------------------------------- + public function __construct() + { + $this->runLength = array_fill(0, QRSPEC_WIDTH_MAX + 1, 0); + } + + //---------------------------------------------------------------------- + public function writeFormatInformation($width, &$frame, $mask, $level) + { + $blacks = 0; + $format = QRspec::getFormatInfo($mask, $level); + + for($i=0; $i<8; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[8][$width - 1 - $i] = chr($v); + if($i < 6) { + $frame[$i][8] = chr($v); + } else { + $frame[$i + 1][8] = chr($v); + } + $format = $format >> 1; + } + + for($i=0; $i<7; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[$width - 7 + $i][8] = chr($v); + if($i == 0) { + $frame[8][7] = chr($v); + } else { + $frame[8][6 - $i] = chr($v); + } + + $format = $format >> 1; + } + + return $blacks; + } + + //---------------------------------------------------------------------- + public function mask0($x, $y) { return ($x+$y)&1; } + public function mask1($x, $y) { return ($y&1); } + public function mask2($x, $y) { return ($x%3); } + public function mask3($x, $y) { return ($x+$y)%3; } + public function mask4($x, $y) { return (((int)($y/2))+((int)($x/3)))&1; } + public function mask5($x, $y) { return (($x*$y)&1)+($x*$y)%3; } + public function mask6($x, $y) { return ((($x*$y)&1)+($x*$y)%3)&1; } + public function mask7($x, $y) { return ((($x*$y)%3)+(($x+$y)&1))&1; } + + //---------------------------------------------------------------------- + private function generateMaskNo($maskNo, $width, $frame) + { + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if(ord($frame[$y][$x]) & 0x80) { + $bitMask[$y][$x] = 0; + } else { + $maskFunc = call_user_func(array($this, 'mask'.$maskNo), $x, $y); + $bitMask[$y][$x] = ($maskFunc == 0)?1:0; + } + + } + } + + return $bitMask; + } + + //---------------------------------------------------------------------- + public static function serial($bitFrame) + { + $codeArr = array(); + + foreach ($bitFrame as $line) + $codeArr[] = join('', $line); + + return gzcompress(join("\n", $codeArr), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + $codeArr = array(); + + $codeLines = explode("\n", gzuncompress($code)); + foreach ($codeLines as $line) + $codeArr[] = str_split($line); + + return $codeArr; + } + + //---------------------------------------------------------------------- + public function makeMaskNo($maskNo, $width, $s, &$d, $maskGenOnly = false) + { + $b = 0; + $bitMask = array(); + + $fileName = QR_CACHE_DIR.'mask_'.$maskNo.DIRECTORY_SEPARATOR.'mask_'.$width.'_'.$maskNo.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + $bitMask = self::unserial(file_get_contents($fileName)); + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + if (!file_exists(QR_CACHE_DIR.'mask_'.$maskNo)) + mkdir(QR_CACHE_DIR.'mask_'.$maskNo); + file_put_contents($fileName, self::serial($bitMask)); + } + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + } + + if ($maskGenOnly) + return; + + $d = $s; + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if($bitMask[$y][$x] == 1) { + $d[$y][$x] = chr(ord($s[$y][$x]) ^ (int)$bitMask[$y][$x]); + } + $b += (int)(ord($d[$y][$x]) & 1); + } + } + + return $b; + } + + //---------------------------------------------------------------------- + public function makeMask($width, $frame, $maskNo, $level) + { + $masked = array_fill(0, $width, str_repeat("\0", $width)); + $this->makeMaskNo($maskNo, $width, $frame, $masked); + $this->writeFormatInformation($width, $masked, $maskNo, $level); + + return $masked; + } + + //---------------------------------------------------------------------- + public function calcN1N3($length) + { + $demerit = 0; + + for($i=0; $i<$length; $i++) { + + if($this->runLength[$i] >= 5) { + $demerit += (N1 + ($this->runLength[$i] - 5)); + } + if($i & 1) { + if(($i >= 3) && ($i < ($length-2)) && ($this->runLength[$i] % 3 == 0)) { + $fact = (int)($this->runLength[$i] / 3); + if(($this->runLength[$i-2] == $fact) && + ($this->runLength[$i-1] == $fact) && + ($this->runLength[$i+1] == $fact) && + ($this->runLength[$i+2] == $fact)) { + if(($this->runLength[$i-3] < 0) || ($this->runLength[$i-3] >= (4 * $fact))) { + $demerit += N3; + } else if((($i+3) >= $length) || ($this->runLength[$i+3] >= (4 * $fact))) { + $demerit += N3; + } + } + } + } + } + return $demerit; + } + + //---------------------------------------------------------------------- + public function evaluateSymbol($width, $frame) + { + $head = 0; + $demerit = 0; + + for($y=0; $y<$width; $y++) { + $head = 0; + $this->runLength[0] = 1; + + $frameY = $frame[$y]; + + if ($y>0) + $frameYM = $frame[$y-1]; + + for($x=0; $x<$width; $x++) { + if(($x > 0) && ($y > 0)) { + $b22 = ord($frameY[$x]) & ord($frameY[$x-1]) & ord($frameYM[$x]) & ord($frameYM[$x-1]); + $w22 = ord($frameY[$x]) | ord($frameY[$x-1]) | ord($frameYM[$x]) | ord($frameYM[$x-1]); + + if(($b22 | ($w22 ^ 1))&1) { + $demerit += N2; + } + } + if(($x == 0) && (ord($frameY[$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($x > 0) { + if((ord($frameY[$x]) ^ ord($frameY[$x-1])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + for($x=0; $x<$width; $x++) { + $head = 0; + $this->runLength[0] = 1; + + for($y=0; $y<$width; $y++) { + if($y == 0 && (ord($frame[$y][$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($y > 0) { + if((ord($frame[$y][$x]) ^ ord($frame[$y-1][$x])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + return $demerit; + } + + + //---------------------------------------------------------------------- + public function mask($width, $frame, $level) + { + $minDemerit = PHP_INT_MAX; + $bestMaskNum = 0; + $bestMask = array(); + + $checked_masks = array(0,1,2,3,4,5,6,7); + + if (QR_FIND_FROM_RANDOM !== false) { + + $howManuOut = 8-(QR_FIND_FROM_RANDOM % 9); + for ($i = 0; $i < $howManuOut; $i++) { + $remPos = rand (0, count($checked_masks)-1); + unset($checked_masks[$remPos]); + $checked_masks = array_values($checked_masks); + } + + } + + $bestMask = $frame; + + foreach($checked_masks as $i) { + $mask = array_fill(0, $width, str_repeat("\0", $width)); + + $demerit = 0; + $blacks = 0; + $blacks = $this->makeMaskNo($i, $width, $frame, $mask); + $blacks += $this->writeFormatInformation($width, $mask, $i, $level); + $blacks = (int)(100 * $blacks / ($width * $width)); + $demerit = (int)((int)(abs($blacks - 50) / 5) * N4); + $demerit += $this->evaluateSymbol($width, $mask); + + if($demerit < $minDemerit) { + $minDemerit = $demerit; + $bestMask = $mask; + $bestMaskNum = $i; + } + } + + return $bestMask; + } + + //---------------------------------------------------------------------- + } + + + + +//---- qrencode.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Main encoder classes. + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsblock { + public $dataLength; + public $data = array(); + public $eccLength; + public $ecc = array(); + + public function __construct($dl, $data, $el, &$ecc, QRrsItem $rs) + { + $rs->encode_rs_char($data, $ecc); + + $this->dataLength = $dl; + $this->data = $data; + $this->eccLength = $el; + $this->ecc = $ecc; + } + }; + + //########################################################################## + + class QRrawcode { + public $version; + public $datacode = array(); + public $ecccode = array(); + public $blocks; + public $rsblocks = array(); //of RSblock + public $count; + public $dataLength; + public $eccLength; + public $b1; + + //---------------------------------------------------------------------- + public function __construct(QRinput $input) + { + $spec = array(0,0,0,0,0); + + $this->datacode = $input->getByteStream(); + if(is_null($this->datacode)) { + throw new Exception('null imput string'); + } + + QRspec::getEccSpec($input->getVersion(), $input->getErrorCorrectionLevel(), $spec); + + $this->version = $input->getVersion(); + $this->b1 = QRspec::rsBlockNum1($spec); + $this->dataLength = QRspec::rsDataLength($spec); + $this->eccLength = QRspec::rsEccLength($spec); + $this->ecccode = array_fill(0, $this->eccLength, 0); + $this->blocks = QRspec::rsBlockNum($spec); + + $ret = $this->init($spec); + if($ret < 0) { + throw new Exception('block alloc error'); + return null; + } + + $this->count = 0; + } + + //---------------------------------------------------------------------- + public function init(array $spec) + { + $dl = QRspec::rsDataCodes1($spec); + $el = QRspec::rsEccCodes1($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + + $blockNo = 0; + $dataPos = 0; + $eccPos = 0; + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + if(QRspec::rsBlockNum2($spec) == 0) + return 0; + + $dl = QRspec::rsDataCodes2($spec); + $el = QRspec::rsEccCodes2($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + if($rs == NULL) return -1; + + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + return 0; + } + + //---------------------------------------------------------------------- + public function getCode() + { + $ret; + + if($this->count < $this->dataLength) { + $row = $this->count % $this->blocks; + $col = $this->count / $this->blocks; + if($col >= $this->rsblocks[0]->dataLength) { + $row += $this->b1; + } + $ret = $this->rsblocks[$row]->data[$col]; + } else if($this->count < $this->dataLength + $this->eccLength) { + $row = ($this->count - $this->dataLength) % $this->blocks; + $col = ($this->count - $this->dataLength) / $this->blocks; + $ret = $this->rsblocks[$row]->ecc[$col]; + } else { + return 0; + } + $this->count++; + + return $ret; + } + } + + //########################################################################## + + class QRcode { + + public $version; + public $width; + public $data; + + //---------------------------------------------------------------------- + public function encodeMask(QRinput $input, $mask) + { + if($input->getVersion() < 0 || $input->getVersion() > QRSPEC_VERSION_MAX) { + throw new Exception('wrong version'); + } + if($input->getErrorCorrectionLevel() > QR_ECLEVEL_H) { + throw new Exception('wrong level'); + } + + $raw = new QRrawcode($input); + + QRtools::markTime('after_raw'); + + $version = $raw->version; + $width = QRspec::getWidth($version); + $frame = QRspec::newFrame($version); + + $filler = new FrameFiller($width, $frame); + if(is_null($filler)) { + return NULL; + } + + // inteleaved data and ecc codes + for($i=0; $i<$raw->dataLength + $raw->eccLength; $i++) { + $code = $raw->getCode(); + $bit = 0x80; + for($j=0; $j<8; $j++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02 | (($bit & $code) != 0)); + $bit = $bit >> 1; + } + } + + QRtools::markTime('after_filler'); + + unset($raw); + + // remainder bits + $j = QRspec::getRemainder($version); + for($i=0; $i<$j; $i++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02); + } + + $frame = $filler->frame; + unset($filler); + + + // masking + $maskObj = new QRmask(); + if($mask < 0) { + + if (QR_FIND_BEST_MASK) { + $masked = $maskObj->mask($width, $frame, $input->getErrorCorrectionLevel()); + } else { + $masked = $maskObj->makeMask($width, $frame, (intval(QR_DEFAULT_MASK) % 8), $input->getErrorCorrectionLevel()); + } + } else { + $masked = $maskObj->makeMask($width, $frame, $mask, $input->getErrorCorrectionLevel()); + } + + if($masked == NULL) { + return NULL; + } + + QRtools::markTime('after_mask'); + + $this->version = $version; + $this->width = $width; + $this->data = $masked; + + return $this; + } + + //---------------------------------------------------------------------- + public function encodeInput(QRinput $input) + { + return $this->encodeMask($input, -1); + } + + //---------------------------------------------------------------------- + public function encodeString8bit($string, $version, $level) + { + if(string == NULL) { + throw new Exception('empty string!'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = $input->append($input, QR_MODE_8, strlen($string), str_split($string)); + if($ret < 0) { + unset($input); + return NULL; + } + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public function encodeString($string, $version, $level, $hint, $casesensitive) + { + + if($hint != QR_MODE_8 && $hint != QR_MODE_KANJI) { + throw new Exception('bad hint'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = QRsplit::splitStringToQRinput($string, $input, $hint, $casesensitive); + if($ret < 0) { + return NULL; + } + + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public static function png($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4, $saveandprint=false) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodePNG($text, $outfile, $saveandprint=false); + } + + //---------------------------------------------------------------------- + public static function text($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encode($text, $outfile); + } + + //---------------------------------------------------------------------- + public static function raw($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodeRAW($text, $outfile); + } + } + + //########################################################################## + + class FrameFiller { + + public $width; + public $frame; + public $x; + public $y; + public $dir; + public $bit; + + //---------------------------------------------------------------------- + public function __construct($width, &$frame) + { + $this->width = $width; + $this->frame = $frame; + $this->x = $width - 1; + $this->y = $width - 1; + $this->dir = -1; + $this->bit = -1; + } + + //---------------------------------------------------------------------- + public function setFrameAt($at, $val) + { + $this->frame[$at['y']][$at['x']] = chr($val); + } + + //---------------------------------------------------------------------- + public function getFrameAt($at) + { + return ord($this->frame[$at['y']][$at['x']]); + } + + //---------------------------------------------------------------------- + public function next() + { + do { + + if($this->bit == -1) { + $this->bit = 0; + return array('x'=>$this->x, 'y'=>$this->y); + } + + $x = $this->x; + $y = $this->y; + $w = $this->width; + + if($this->bit == 0) { + $x--; + $this->bit++; + } else { + $x++; + $y += $this->dir; + $this->bit--; + } + + if($this->dir < 0) { + if($y < 0) { + $y = 0; + $x -= 2; + $this->dir = 1; + if($x == 6) { + $x--; + $y = 9; + } + } + } else { + if($y == $w) { + $y = $w - 1; + $x -= 2; + $this->dir = -1; + if($x == 6) { + $x--; + $y -= 8; + } + } + } + if($x < 0 || $y < 0) return null; + + $this->x = $x; + $this->y = $y; + + } while(ord($this->frame[$y][$x]) & 0x80); + + return array('x'=>$x, 'y'=>$y); + } + + } ; + + //########################################################################## + + class QRencode { + + public $casesensitive = true; + public $eightbit = false; + + public $version = 0; + public $size = 3; + public $margin = 4; + + public $structured = 0; // not supported yet + + public $level = QR_ECLEVEL_L; + public $hint = QR_MODE_8; + + //---------------------------------------------------------------------- + public static function factory($level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = new QRencode(); + $enc->size = $size; + $enc->margin = $margin; + + switch ($level.'') { + case '0': + case '1': + case '2': + case '3': + $enc->level = $level; + break; + case 'l': + case 'L': + $enc->level = QR_ECLEVEL_L; + break; + case 'm': + case 'M': + $enc->level = QR_ECLEVEL_M; + break; + case 'q': + case 'Q': + $enc->level = QR_ECLEVEL_Q; + break; + case 'h': + case 'H': + $enc->level = QR_ECLEVEL_H; + break; + } + + return $enc; + } + + //---------------------------------------------------------------------- + public function encodeRAW($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + return $code->data; + } + + //---------------------------------------------------------------------- + public function encode($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + QRtools::markTime('after_encode'); + + if ($outfile!== false) { + file_put_contents($outfile, join("\n", QRtools::binarize($code->data))); + } else { + return QRtools::binarize($code->data); + } + } + + //---------------------------------------------------------------------- + public function encodePNG($intext, $outfile = false,$saveandprint=false) + { + try { + + ob_start(); + $tab = $this->encode($intext); + $err = ob_get_contents(); + ob_end_clean(); + + if ($err != '') + QRtools::log($outfile, $err); + + $maxSize = (int)(QR_PNG_MAXIMUM_SIZE / (count($tab)+2*$this->margin)); + + QRimage::png($tab, $outfile, min(max(1, $this->size), $maxSize), $this->margin,$saveandprint); + + } catch (Exception $e) { + + QRtools::log($outfile, $e->getMessage()); + + } + } + } + + diff --git a/application/libraries/phpqrcode/qrbitstream.php b/application/libraries/phpqrcode/qrbitstream.php new file mode 100644 index 0000000..7d4ec4a --- /dev/null +++ b/application/libraries/phpqrcode/qrbitstream.php @@ -0,0 +1,180 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRbitstream { + + public $data = array(); + + //---------------------------------------------------------------------- + public function size() + { + return count($this->data); + } + + //---------------------------------------------------------------------- + public function allocate($setLength) + { + $this->data = array_fill(0, $setLength, 0); + return 0; + } + + //---------------------------------------------------------------------- + public static function newFromNum($bits, $num) + { + $bstream = new QRbitstream(); + $bstream->allocate($bits); + + $mask = 1 << ($bits - 1); + for($i=0; $i<$bits; $i++) { + if($num & $mask) { + $bstream->data[$i] = 1; + } else { + $bstream->data[$i] = 0; + } + $mask = $mask >> 1; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public static function newFromBytes($size, $data) + { + $bstream = new QRbitstream(); + $bstream->allocate($size * 8); + $p=0; + + for($i=0; $i<$size; $i++) { + $mask = 0x80; + for($j=0; $j<8; $j++) { + if($data[$i] & $mask) { + $bstream->data[$p] = 1; + } else { + $bstream->data[$p] = 0; + } + $p++; + $mask = $mask >> 1; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function append(QRbitstream $arg) + { + if (is_null($arg)) { + return -1; + } + + if($arg->size() == 0) { + return 0; + } + + if($this->size() == 0) { + $this->data = $arg->data; + return 0; + } + + $this->data = array_values(array_merge($this->data, $arg->data)); + + return 0; + } + + //---------------------------------------------------------------------- + public function appendNum($bits, $num) + { + if ($bits == 0) + return 0; + + $b = QRbitstream::newFromNum($bits, $num); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function appendBytes($size, $data) + { + if ($size == 0) + return 0; + + $b = QRbitstream::newFromBytes($size, $data); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function toByte() + { + + $size = $this->size(); + + if($size == 0) { + return array(); + } + + $data = array_fill(0, (int)(($size + 7) / 8), 0); + $bytes = (int)($size / 8); + + $p = 0; + + for($i=0; $i<$bytes; $i++) { + $v = 0; + for($j=0; $j<8; $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$i] = $v; + } + + if($size & 7) { + $v = 0; + for($j=0; $j<($size & 7); $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$bytes] = $v; + } + + return $data; + } + + } diff --git a/application/libraries/phpqrcode/qrconfig.php b/application/libraries/phpqrcode/qrconfig.php new file mode 100644 index 0000000..e53dff8 --- /dev/null +++ b/application/libraries/phpqrcode/qrconfig.php @@ -0,0 +1,17 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + // Encoding modes + + define('QR_MODE_NUL', -1); + define('QR_MODE_NUM', 0); + define('QR_MODE_AN', 1); + define('QR_MODE_8', 2); + define('QR_MODE_KANJI', 3); + define('QR_MODE_STRUCTURE', 4); + + // Levels of error correction. + + define('QR_ECLEVEL_L', 0); + define('QR_ECLEVEL_M', 1); + define('QR_ECLEVEL_Q', 2); + define('QR_ECLEVEL_H', 3); + + // Supported output formats + + define('QR_FORMAT_TEXT', 0); + define('QR_FORMAT_PNG', 1); + + class qrstr { + public static function set(&$srctab, $x, $y, $repl, $replLen = false) { + $srctab[$y] = substr_replace($srctab[$y], ($replLen !== false)?substr($repl,0,$replLen):$repl, $x, ($replLen !== false)?$replLen:strlen($repl)); + } + } \ No newline at end of file diff --git a/application/libraries/phpqrcode/qrencode.php b/application/libraries/phpqrcode/qrencode.php new file mode 100644 index 0000000..4b77a5b --- /dev/null +++ b/application/libraries/phpqrcode/qrencode.php @@ -0,0 +1,502 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsblock { + public $dataLength; + public $data = array(); + public $eccLength; + public $ecc = array(); + + public function __construct($dl, $data, $el, &$ecc, QRrsItem $rs) + { + $rs->encode_rs_char($data, $ecc); + + $this->dataLength = $dl; + $this->data = $data; + $this->eccLength = $el; + $this->ecc = $ecc; + } + }; + + //########################################################################## + + class QRrawcode { + public $version; + public $datacode = array(); + public $ecccode = array(); + public $blocks; + public $rsblocks = array(); //of RSblock + public $count; + public $dataLength; + public $eccLength; + public $b1; + + //---------------------------------------------------------------------- + public function __construct(QRinput $input) + { + $spec = array(0,0,0,0,0); + + $this->datacode = $input->getByteStream(); + if(is_null($this->datacode)) { + throw new Exception('null imput string'); + } + + QRspec::getEccSpec($input->getVersion(), $input->getErrorCorrectionLevel(), $spec); + + $this->version = $input->getVersion(); + $this->b1 = QRspec::rsBlockNum1($spec); + $this->dataLength = QRspec::rsDataLength($spec); + $this->eccLength = QRspec::rsEccLength($spec); + $this->ecccode = array_fill(0, $this->eccLength, 0); + $this->blocks = QRspec::rsBlockNum($spec); + + $ret = $this->init($spec); + if($ret < 0) { + throw new Exception('block alloc error'); + return null; + } + + $this->count = 0; + } + + //---------------------------------------------------------------------- + public function init(array $spec) + { + $dl = QRspec::rsDataCodes1($spec); + $el = QRspec::rsEccCodes1($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + + $blockNo = 0; + $dataPos = 0; + $eccPos = 0; + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + if(QRspec::rsBlockNum2($spec) == 0) + return 0; + + $dl = QRspec::rsDataCodes2($spec); + $el = QRspec::rsEccCodes2($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + if($rs == NULL) return -1; + + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + return 0; + } + + //---------------------------------------------------------------------- + public function getCode() + { + $ret; + + if($this->count < $this->dataLength) { + $row = $this->count % $this->blocks; + $col = $this->count / $this->blocks; + if($col >= $this->rsblocks[0]->dataLength) { + $row += $this->b1; + } + $ret = $this->rsblocks[$row]->data[$col]; + } else if($this->count < $this->dataLength + $this->eccLength) { + $row = ($this->count - $this->dataLength) % $this->blocks; + $col = ($this->count - $this->dataLength) / $this->blocks; + $ret = $this->rsblocks[$row]->ecc[$col]; + } else { + return 0; + } + $this->count++; + + return $ret; + } + } + + //########################################################################## + + class QRcode { + + public $version; + public $width; + public $data; + + //---------------------------------------------------------------------- + public function encodeMask(QRinput $input, $mask) + { + if($input->getVersion() < 0 || $input->getVersion() > QRSPEC_VERSION_MAX) { + throw new Exception('wrong version'); + } + if($input->getErrorCorrectionLevel() > QR_ECLEVEL_H) { + throw new Exception('wrong level'); + } + + $raw = new QRrawcode($input); + + QRtools::markTime('after_raw'); + + $version = $raw->version; + $width = QRspec::getWidth($version); + $frame = QRspec::newFrame($version); + + $filler = new FrameFiller($width, $frame); + if(is_null($filler)) { + return NULL; + } + + // inteleaved data and ecc codes + for($i=0; $i<$raw->dataLength + $raw->eccLength; $i++) { + $code = $raw->getCode(); + $bit = 0x80; + for($j=0; $j<8; $j++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02 | (($bit & $code) != 0)); + $bit = $bit >> 1; + } + } + + QRtools::markTime('after_filler'); + + unset($raw); + + // remainder bits + $j = QRspec::getRemainder($version); + for($i=0; $i<$j; $i++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02); + } + + $frame = $filler->frame; + unset($filler); + + + // masking + $maskObj = new QRmask(); + if($mask < 0) { + + if (QR_FIND_BEST_MASK) { + $masked = $maskObj->mask($width, $frame, $input->getErrorCorrectionLevel()); + } else { + $masked = $maskObj->makeMask($width, $frame, (intval(QR_DEFAULT_MASK) % 8), $input->getErrorCorrectionLevel()); + } + } else { + $masked = $maskObj->makeMask($width, $frame, $mask, $input->getErrorCorrectionLevel()); + } + + if($masked == NULL) { + return NULL; + } + + QRtools::markTime('after_mask'); + + $this->version = $version; + $this->width = $width; + $this->data = $masked; + + return $this; + } + + //---------------------------------------------------------------------- + public function encodeInput(QRinput $input) + { + return $this->encodeMask($input, -1); + } + + //---------------------------------------------------------------------- + public function encodeString8bit($string, $version, $level) + { + if(string == NULL) { + throw new Exception('empty string!'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = $input->append($input, QR_MODE_8, strlen($string), str_split($string)); + if($ret < 0) { + unset($input); + return NULL; + } + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public function encodeString($string, $version, $level, $hint, $casesensitive) + { + + if($hint != QR_MODE_8 && $hint != QR_MODE_KANJI) { + throw new Exception('bad hint'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = QRsplit::splitStringToQRinput($string, $input, $hint, $casesensitive); + if($ret < 0) { + return NULL; + } + + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public static function png($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4, $saveandprint=false) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodePNG($text, $outfile, $saveandprint=false); + } + + //---------------------------------------------------------------------- + public static function text($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encode($text, $outfile); + } + + //---------------------------------------------------------------------- + public static function raw($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodeRAW($text, $outfile); + } + } + + //########################################################################## + + class FrameFiller { + + public $width; + public $frame; + public $x; + public $y; + public $dir; + public $bit; + + //---------------------------------------------------------------------- + public function __construct($width, &$frame) + { + $this->width = $width; + $this->frame = $frame; + $this->x = $width - 1; + $this->y = $width - 1; + $this->dir = -1; + $this->bit = -1; + } + + //---------------------------------------------------------------------- + public function setFrameAt($at, $val) + { + $this->frame[$at['y']][$at['x']] = chr($val); + } + + //---------------------------------------------------------------------- + public function getFrameAt($at) + { + return ord($this->frame[$at['y']][$at['x']]); + } + + //---------------------------------------------------------------------- + public function next() + { + do { + + if($this->bit == -1) { + $this->bit = 0; + return array('x'=>$this->x, 'y'=>$this->y); + } + + $x = $this->x; + $y = $this->y; + $w = $this->width; + + if($this->bit == 0) { + $x--; + $this->bit++; + } else { + $x++; + $y += $this->dir; + $this->bit--; + } + + if($this->dir < 0) { + if($y < 0) { + $y = 0; + $x -= 2; + $this->dir = 1; + if($x == 6) { + $x--; + $y = 9; + } + } + } else { + if($y == $w) { + $y = $w - 1; + $x -= 2; + $this->dir = -1; + if($x == 6) { + $x--; + $y -= 8; + } + } + } + if($x < 0 || $y < 0) return null; + + $this->x = $x; + $this->y = $y; + + } while(ord($this->frame[$y][$x]) & 0x80); + + return array('x'=>$x, 'y'=>$y); + } + + } ; + + //########################################################################## + + class QRencode { + + public $casesensitive = true; + public $eightbit = false; + + public $version = 0; + public $size = 3; + public $margin = 4; + + public $structured = 0; // not supported yet + + public $level = QR_ECLEVEL_L; + public $hint = QR_MODE_8; + + //---------------------------------------------------------------------- + public static function factory($level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = new QRencode(); + $enc->size = $size; + $enc->margin = $margin; + + switch ($level.'') { + case '0': + case '1': + case '2': + case '3': + $enc->level = $level; + break; + case 'l': + case 'L': + $enc->level = QR_ECLEVEL_L; + break; + case 'm': + case 'M': + $enc->level = QR_ECLEVEL_M; + break; + case 'q': + case 'Q': + $enc->level = QR_ECLEVEL_Q; + break; + case 'h': + case 'H': + $enc->level = QR_ECLEVEL_H; + break; + } + + return $enc; + } + + //---------------------------------------------------------------------- + public function encodeRAW($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + return $code->data; + } + + //---------------------------------------------------------------------- + public function encode($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + QRtools::markTime('after_encode'); + + if ($outfile!== false) { + file_put_contents($outfile, join("\n", QRtools::binarize($code->data))); + } else { + return QRtools::binarize($code->data); + } + } + + //---------------------------------------------------------------------- + public function encodePNG($intext, $outfile = false,$saveandprint=false) + { + try { + + ob_start(); + $tab = $this->encode($intext); + $err = ob_get_contents(); + ob_end_clean(); + + if ($err != '') + QRtools::log($outfile, $err); + + $maxSize = (int)(QR_PNG_MAXIMUM_SIZE / (count($tab)+2*$this->margin)); + + QRimage::png($tab, $outfile, min(max(1, $this->size), $maxSize), $this->margin,$saveandprint); + + } catch (Exception $e) { + + QRtools::log($outfile, $e->getMessage()); + + } + } + } diff --git a/application/libraries/phpqrcode/qrimage.php b/application/libraries/phpqrcode/qrimage.php new file mode 100644 index 0000000..10b0a6e --- /dev/null +++ b/application/libraries/phpqrcode/qrimage.php @@ -0,0 +1,95 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QR_IMAGE', true); + + class QRimage { + + //---------------------------------------------------------------------- + public static function png($frame, $filename = false, $pixelPerPoint = 4, $outerFrame = 4,$saveandprint=FALSE) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/png"); + ImagePng($image); + } else { + if($saveandprint===TRUE){ + ImagePng($image, $filename); + header("Content-type: image/png"); + ImagePng($image); + }else{ + ImagePng($image, $filename); + } + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + public static function jpg($frame, $filename = false, $pixelPerPoint = 8, $outerFrame = 4, $q = 85) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/jpeg"); + ImageJpeg($image, null, $q); + } else { + ImageJpeg($image, $filename, $q); + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + private static function image($frame, $pixelPerPoint = 4, $outerFrame = 4) + { + $h = count($frame); + $w = strlen($frame[0]); + + $imgW = $w + 2*$outerFrame; + $imgH = $h + 2*$outerFrame; + + $base_image =ImageCreate($imgW, $imgH); + + $col[0] = ImageColorAllocate($base_image,255,255,255); + $col[1] = ImageColorAllocate($base_image,0,0,0); + + imagefill($base_image, 0, 0, $col[0]); + + for($y=0; $y<$h; $y++) { + for($x=0; $x<$w; $x++) { + if ($frame[$y][$x] == '1') { + ImageSetPixel($base_image,$x+$outerFrame,$y+$outerFrame,$col[1]); + } + } + } + + $target_image =ImageCreate($imgW * $pixelPerPoint, $imgH * $pixelPerPoint); + ImageCopyResized($target_image, $base_image, 0, 0, 0, 0, $imgW * $pixelPerPoint, $imgH * $pixelPerPoint, $imgW, $imgH); + ImageDestroy($base_image); + + return $target_image; + } + } \ No newline at end of file diff --git a/application/libraries/phpqrcode/qrinput.php b/application/libraries/phpqrcode/qrinput.php new file mode 100644 index 0000000..0f6d7f9 --- /dev/null +++ b/application/libraries/phpqrcode/qrinput.php @@ -0,0 +1,729 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('STRUCTURE_HEADER_BITS', 20); + define('MAX_STRUCTURED_SYMBOLS', 16); + + class QRinputItem { + + public $mode; + public $size; + public $data; + public $bstream; + + public function __construct($mode, $size, $data, $bstream = null) + { + $setData = array_slice($data, 0, $size); + + if (count($setData) < $size) { + $setData = array_merge($setData, array_fill(0,$size-count($setData),0)); + } + + if(!QRinput::check($mode, $size, $setData)) { + throw new Exception('Error m:'.$mode.',s:'.$size.',d:'.join(',',$setData)); + return null; + } + + $this->mode = $mode; + $this->size = $size; + $this->data = $setData; + $this->bstream = $bstream; + } + + //---------------------------------------------------------------------- + public function encodeModeNum($version) + { + try { + + $words = (int)($this->size / 3); + $bs = new QRbitstream(); + + $val = 0x1; + $bs->appendNum(4, $val); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_NUM, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (ord($this->data[$i*3 ]) - ord('0')) * 100; + $val += (ord($this->data[$i*3+1]) - ord('0')) * 10; + $val += (ord($this->data[$i*3+2]) - ord('0')); + $bs->appendNum(10, $val); + } + + if($this->size - $words * 3 == 1) { + $val = ord($this->data[$words*3]) - ord('0'); + $bs->appendNum(4, $val); + } else if($this->size - $words * 3 == 2) { + $val = (ord($this->data[$words*3 ]) - ord('0')) * 10; + $val += (ord($this->data[$words*3+1]) - ord('0')); + $bs->appendNum(7, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeAn($version) + { + try { + $words = (int)($this->size / 2); + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x02); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_AN, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (int)QRinput::lookAnTable(ord($this->data[$i*2 ])) * 45; + $val += (int)QRinput::lookAnTable(ord($this->data[$i*2+1])); + + $bs->appendNum(11, $val); + } + + if($this->size & 1) { + $val = QRinput::lookAnTable(ord($this->data[$words * 2])); + $bs->appendNum(6, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeMode8($version) + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x4); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_8, $version), $this->size); + + for($i=0; $i<$this->size; $i++) { + $bs->appendNum(8, ord($this->data[$i])); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeKanji($version) + { + try { + + $bs = new QRbitrtream(); + + $bs->appendNum(4, 0x8); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_KANJI, $version), (int)($this->size / 2)); + + for($i=0; $i<$this->size; $i+=2) { + $val = (ord($this->data[$i]) << 8) | ord($this->data[$i+1]); + if($val <= 0x9ffc) { + $val -= 0x8140; + } else { + $val -= 0xc140; + } + + $h = ($val >> 8) * 0xc0; + $val = ($val & 0xff) + $h; + + $bs->appendNum(13, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeStructure() + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x03); + $bs->appendNum(4, ord($this->data[1]) - 1); + $bs->appendNum(4, ord($this->data[0]) - 1); + $bs->appendNum(8, ord($this->data[2])); + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function estimateBitStreamSizeOfEntry($version) + { + $bits = 0; + + if($version == 0) + $version = 1; + + switch($this->mode) { + case QR_MODE_NUM: $bits = QRinput::estimateBitsModeNum($this->size); break; + case QR_MODE_AN: $bits = QRinput::estimateBitsModeAn($this->size); break; + case QR_MODE_8: $bits = QRinput::estimateBitsMode8($this->size); break; + case QR_MODE_KANJI: $bits = QRinput::estimateBitsModeKanji($this->size);break; + case QR_MODE_STRUCTURE: return STRUCTURE_HEADER_BITS; + default: + return 0; + } + + $l = QRspec::lengthIndicator($this->mode, $version); + $m = 1 << $l; + $num = (int)(($this->size + $m - 1) / $m); + + $bits += $num * (4 + $l); + + return $bits; + } + + //---------------------------------------------------------------------- + public function encodeBitStream($version) + { + try { + + unset($this->bstream); + $words = QRspec::maximumWords($this->mode, $version); + + if($this->size > $words) { + + $st1 = new QRinputItem($this->mode, $words, $this->data); + $st2 = new QRinputItem($this->mode, $this->size - $words, array_slice($this->data, $words)); + + $st1->encodeBitStream($version); + $st2->encodeBitStream($version); + + $this->bstream = new QRbitstream(); + $this->bstream->append($st1->bstream); + $this->bstream->append($st2->bstream); + + unset($st1); + unset($st2); + + } else { + + $ret = 0; + + switch($this->mode) { + case QR_MODE_NUM: $ret = $this->encodeModeNum($version); break; + case QR_MODE_AN: $ret = $this->encodeModeAn($version); break; + case QR_MODE_8: $ret = $this->encodeMode8($version); break; + case QR_MODE_KANJI: $ret = $this->encodeModeKanji($version);break; + case QR_MODE_STRUCTURE: $ret = $this->encodeModeStructure(); break; + + default: + break; + } + + if($ret < 0) + return -1; + } + + return $this->bstream->size(); + + } catch (Exception $e) { + return -1; + } + } + }; + + //########################################################################## + + class QRinput { + + public $items; + + private $version; + private $level; + + //---------------------------------------------------------------------- + public function __construct($version = 0, $level = QR_ECLEVEL_L) + { + if ($version < 0 || $version > QRSPEC_VERSION_MAX || $level > QR_ECLEVEL_H) { + throw new Exception('Invalid version no'); + return NULL; + } + + $this->version = $version; + $this->level = $level; + } + + //---------------------------------------------------------------------- + public function getVersion() + { + return $this->version; + } + + //---------------------------------------------------------------------- + public function setVersion($version) + { + if($version < 0 || $version > QRSPEC_VERSION_MAX) { + throw new Exception('Invalid version no'); + return -1; + } + + $this->version = $version; + + return 0; + } + + //---------------------------------------------------------------------- + public function getErrorCorrectionLevel() + { + return $this->level; + } + + //---------------------------------------------------------------------- + public function setErrorCorrectionLevel($level) + { + if($level > QR_ECLEVEL_H) { + throw new Exception('Invalid ECLEVEL'); + return -1; + } + + $this->level = $level; + + return 0; + } + + //---------------------------------------------------------------------- + public function appendEntry(QRinputItem $entry) + { + $this->items[] = $entry; + } + + //---------------------------------------------------------------------- + public function append($mode, $size, $data) + { + try { + $entry = new QRinputItem($mode, $size, $data); + $this->items[] = $entry; + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + + public function insertStructuredAppendHeader($size, $index, $parity) + { + if( $size > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong size'); + } + + if( $index <= 0 || $index > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong index'); + } + + $buf = array($size, $index, $parity); + + try { + $entry = new QRinputItem(QR_MODE_STRUCTURE, 3, buf); + array_unshift($this->items, $entry); + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function calcParity() + { + $parity = 0; + + foreach($this->items as $item) { + if($item->mode != QR_MODE_STRUCTURE) { + for($i=$item->size-1; $i>=0; $i--) { + $parity ^= $item->data[$i]; + } + } + } + + return $parity; + } + + //---------------------------------------------------------------------- + public static function checkModeNum($size, $data) + { + for($i=0; $i<$size; $i++) { + if((ord($data[$i]) < ord('0')) || (ord($data[$i]) > ord('9'))){ + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeNum($size) + { + $w = (int)$size / 3; + $bits = $w * 10; + + switch($size - $w * 3) { + case 1: + $bits += 4; + break; + case 2: + $bits += 7; + break; + default: + break; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static $anTable = array( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ); + + //---------------------------------------------------------------------- + public static function lookAnTable($c) + { + return (($c > 127)?-1:self::$anTable[$c]); + } + + //---------------------------------------------------------------------- + public static function checkModeAn($size, $data) + { + for($i=0; $i<$size; $i++) { + if (self::lookAnTable(ord($data[$i])) == -1) { + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeAn($size) + { + $w = (int)($size / 2); + $bits = $w * 11; + + if($size & 1) { + $bits += 6; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static function estimateBitsMode8($size) + { + return $size * 8; + } + + //---------------------------------------------------------------------- + public function estimateBitsModeKanji($size) + { + return (int)(($size / 2) * 13); + } + + //---------------------------------------------------------------------- + public static function checkModeKanji($size, $data) + { + if($size & 1) + return false; + + for($i=0; $i<$size; $i+=2) { + $val = (ord($data[$i]) << 8) | ord($data[$i+1]); + if( $val < 0x8140 + || ($val > 0x9ffc && $val < 0xe040) + || $val > 0xebbf) { + return false; + } + } + + return true; + } + + /*********************************************************************** + * Validation + **********************************************************************/ + + public static function check($mode, $size, $data) + { + if($size <= 0) + return false; + + switch($mode) { + case QR_MODE_NUM: return self::checkModeNum($size, $data); break; + case QR_MODE_AN: return self::checkModeAn($size, $data); break; + case QR_MODE_KANJI: return self::checkModeKanji($size, $data); break; + case QR_MODE_8: return true; break; + case QR_MODE_STRUCTURE: return true; break; + + default: + break; + } + + return false; + } + + + //---------------------------------------------------------------------- + public function estimateBitStreamSize($version) + { + $bits = 0; + + foreach($this->items as $item) { + $bits += $item->estimateBitStreamSizeOfEntry($version); + } + + return $bits; + } + + //---------------------------------------------------------------------- + public function estimateVersion() + { + $version = 0; + $prev = 0; + do { + $prev = $version; + $bits = $this->estimateBitStreamSize($prev); + $version = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if ($version < 0) { + return -1; + } + } while ($version > $prev); + + return $version; + } + + //---------------------------------------------------------------------- + public static function lengthOfCode($mode, $version, $bits) + { + $payload = $bits - 4 - QRspec::lengthIndicator($mode, $version); + switch($mode) { + case QR_MODE_NUM: + $chunks = (int)($payload / 10); + $remain = $payload - $chunks * 10; + $size = $chunks * 3; + if($remain >= 7) { + $size += 2; + } else if($remain >= 4) { + $size += 1; + } + break; + case QR_MODE_AN: + $chunks = (int)($payload / 11); + $remain = $payload - $chunks * 11; + $size = $chunks * 2; + if($remain >= 6) + $size++; + break; + case QR_MODE_8: + $size = (int)($payload / 8); + break; + case QR_MODE_KANJI: + $size = (int)(($payload / 13) * 2); + break; + case QR_MODE_STRUCTURE: + $size = (int)($payload / 8); + break; + default: + $size = 0; + break; + } + + $maxsize = QRspec::maximumWords($mode, $version); + if($size < 0) $size = 0; + if($size > $maxsize) $size = $maxsize; + + return $size; + } + + //---------------------------------------------------------------------- + public function createBitStream() + { + $total = 0; + + foreach($this->items as $item) { + $bits = $item->encodeBitStream($this->version); + + if($bits < 0) + return -1; + + $total += $bits; + } + + return $total; + } + + //---------------------------------------------------------------------- + public function convertData() + { + $ver = $this->estimateVersion(); + if($ver > $this->getVersion()) { + $this->setVersion($ver); + } + + for(;;) { + $bits = $this->createBitStream(); + + if($bits < 0) + return -1; + + $ver = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if($ver < 0) { + throw new Exception('WRONG VERSION'); + return -1; + } else if($ver > $this->getVersion()) { + $this->setVersion($ver); + } else { + break; + } + } + + return 0; + } + + //---------------------------------------------------------------------- + public function appendPaddingBit(&$bstream) + { + $bits = $bstream->size(); + $maxwords = QRspec::getDataLength($this->version, $this->level); + $maxbits = $maxwords * 8; + + if ($maxbits == $bits) { + return 0; + } + + if ($maxbits - $bits < 5) { + return $bstream->appendNum($maxbits - $bits, 0); + } + + $bits += 4; + $words = (int)(($bits + 7) / 8); + + $padding = new QRbitstream(); + $ret = $padding->appendNum($words * 8 - $bits + 4, 0); + + if($ret < 0) + return $ret; + + $padlen = $maxwords - $words; + + if($padlen > 0) { + + $padbuf = array(); + for($i=0; $i<$padlen; $i++) { + $padbuf[$i] = ($i&1)?0x11:0xec; + } + + $ret = $padding->appendBytes($padlen, $padbuf); + + if($ret < 0) + return $ret; + + } + + $ret = $bstream->append($padding); + + return $ret; + } + + //---------------------------------------------------------------------- + public function mergeBitStream() + { + if($this->convertData() < 0) { + return null; + } + + $bstream = new QRbitstream(); + + foreach($this->items as $item) { + $ret = $bstream->append($item->bstream); + if($ret < 0) { + return null; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getBitStream() + { + + $bstream = $this->mergeBitStream(); + + if($bstream == null) { + return null; + } + + $ret = $this->appendPaddingBit($bstream); + if($ret < 0) { + return null; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getByteStream() + { + $bstream = $this->getBitStream(); + if($bstream == null) { + return null; + } + + return $bstream->toByte(); + } + } + + + \ No newline at end of file diff --git a/application/libraries/phpqrcode/qrlib.php b/application/libraries/phpqrcode/qrlib.php new file mode 100644 index 0000000..d55c4af --- /dev/null +++ b/application/libraries/phpqrcode/qrlib.php @@ -0,0 +1,43 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + $QR_BASEDIR = dirname(__FILE__).DIRECTORY_SEPARATOR; + + // Required libs + + include $QR_BASEDIR."qrconst.php"; + include $QR_BASEDIR."qrconfig.php"; + include $QR_BASEDIR."qrtools.php"; + include $QR_BASEDIR."qrspec.php"; + include $QR_BASEDIR."qrimage.php"; + include $QR_BASEDIR."qrinput.php"; + include $QR_BASEDIR."qrbitstream.php"; + include $QR_BASEDIR."qrsplit.php"; + include $QR_BASEDIR."qrrscode.php"; + include $QR_BASEDIR."qrmask.php"; + include $QR_BASEDIR."qrencode.php"; + diff --git a/application/libraries/phpqrcode/qrmask.php b/application/libraries/phpqrcode/qrmask.php new file mode 100644 index 0000000..b14d7ae --- /dev/null +++ b/application/libraries/phpqrcode/qrmask.php @@ -0,0 +1,328 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('N1', 3); + define('N2', 3); + define('N3', 40); + define('N4', 10); + + class QRmask { + + public $runLength = array(); + + //---------------------------------------------------------------------- + public function __construct() + { + $this->runLength = array_fill(0, QRSPEC_WIDTH_MAX + 1, 0); + } + + //---------------------------------------------------------------------- + public function writeFormatInformation($width, &$frame, $mask, $level) + { + $blacks = 0; + $format = QRspec::getFormatInfo($mask, $level); + + for($i=0; $i<8; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[8][$width - 1 - $i] = chr($v); + if($i < 6) { + $frame[$i][8] = chr($v); + } else { + $frame[$i + 1][8] = chr($v); + } + $format = $format >> 1; + } + + for($i=0; $i<7; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[$width - 7 + $i][8] = chr($v); + if($i == 0) { + $frame[8][7] = chr($v); + } else { + $frame[8][6 - $i] = chr($v); + } + + $format = $format >> 1; + } + + return $blacks; + } + + //---------------------------------------------------------------------- + public function mask0($x, $y) { return ($x+$y)&1; } + public function mask1($x, $y) { return ($y&1); } + public function mask2($x, $y) { return ($x%3); } + public function mask3($x, $y) { return ($x+$y)%3; } + public function mask4($x, $y) { return (((int)($y/2))+((int)($x/3)))&1; } + public function mask5($x, $y) { return (($x*$y)&1)+($x*$y)%3; } + public function mask6($x, $y) { return ((($x*$y)&1)+($x*$y)%3)&1; } + public function mask7($x, $y) { return ((($x*$y)%3)+(($x+$y)&1))&1; } + + //---------------------------------------------------------------------- + private function generateMaskNo($maskNo, $width, $frame) + { + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if(ord($frame[$y][$x]) & 0x80) { + $bitMask[$y][$x] = 0; + } else { + $maskFunc = call_user_func(array($this, 'mask'.$maskNo), $x, $y); + $bitMask[$y][$x] = ($maskFunc == 0)?1:0; + } + + } + } + + return $bitMask; + } + + //---------------------------------------------------------------------- + public static function serial($bitFrame) + { + $codeArr = array(); + + foreach ($bitFrame as $line) + $codeArr[] = join('', $line); + + return gzcompress(join("\n", $codeArr), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + $codeArr = array(); + + $codeLines = explode("\n", gzuncompress($code)); + foreach ($codeLines as $line) + $codeArr[] = str_split($line); + + return $codeArr; + } + + //---------------------------------------------------------------------- + public function makeMaskNo($maskNo, $width, $s, &$d, $maskGenOnly = false) + { + $b = 0; + $bitMask = array(); + + $fileName = QR_CACHE_DIR.'mask_'.$maskNo.DIRECTORY_SEPARATOR.'mask_'.$width.'_'.$maskNo.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + $bitMask = self::unserial(file_get_contents($fileName)); + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + if (!file_exists(QR_CACHE_DIR.'mask_'.$maskNo)) + mkdir(QR_CACHE_DIR.'mask_'.$maskNo); + file_put_contents($fileName, self::serial($bitMask)); + } + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + } + + if ($maskGenOnly) + return; + + $d = $s; + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if($bitMask[$y][$x] == 1) { + $d[$y][$x] = chr(ord($s[$y][$x]) ^ (int)$bitMask[$y][$x]); + } + $b += (int)(ord($d[$y][$x]) & 1); + } + } + + return $b; + } + + //---------------------------------------------------------------------- + public function makeMask($width, $frame, $maskNo, $level) + { + $masked = array_fill(0, $width, str_repeat("\0", $width)); + $this->makeMaskNo($maskNo, $width, $frame, $masked); + $this->writeFormatInformation($width, $masked, $maskNo, $level); + + return $masked; + } + + //---------------------------------------------------------------------- + public function calcN1N3($length) + { + $demerit = 0; + + for($i=0; $i<$length; $i++) { + + if($this->runLength[$i] >= 5) { + $demerit += (N1 + ($this->runLength[$i] - 5)); + } + if($i & 1) { + if(($i >= 3) && ($i < ($length-2)) && ($this->runLength[$i] % 3 == 0)) { + $fact = (int)($this->runLength[$i] / 3); + if(($this->runLength[$i-2] == $fact) && + ($this->runLength[$i-1] == $fact) && + ($this->runLength[$i+1] == $fact) && + ($this->runLength[$i+2] == $fact)) { + if(($this->runLength[$i-3] < 0) || ($this->runLength[$i-3] >= (4 * $fact))) { + $demerit += N3; + } else if((($i+3) >= $length) || ($this->runLength[$i+3] >= (4 * $fact))) { + $demerit += N3; + } + } + } + } + } + return $demerit; + } + + //---------------------------------------------------------------------- + public function evaluateSymbol($width, $frame) + { + $head = 0; + $demerit = 0; + + for($y=0; $y<$width; $y++) { + $head = 0; + $this->runLength[0] = 1; + + $frameY = $frame[$y]; + + if ($y>0) + $frameYM = $frame[$y-1]; + + for($x=0; $x<$width; $x++) { + if(($x > 0) && ($y > 0)) { + $b22 = ord($frameY[$x]) & ord($frameY[$x-1]) & ord($frameYM[$x]) & ord($frameYM[$x-1]); + $w22 = ord($frameY[$x]) | ord($frameY[$x-1]) | ord($frameYM[$x]) | ord($frameYM[$x-1]); + + if(($b22 | ($w22 ^ 1))&1) { + $demerit += N2; + } + } + if(($x == 0) && (ord($frameY[$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($x > 0) { + if((ord($frameY[$x]) ^ ord($frameY[$x-1])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + for($x=0; $x<$width; $x++) { + $head = 0; + $this->runLength[0] = 1; + + for($y=0; $y<$width; $y++) { + if($y == 0 && (ord($frame[$y][$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($y > 0) { + if((ord($frame[$y][$x]) ^ ord($frame[$y-1][$x])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + return $demerit; + } + + + //---------------------------------------------------------------------- + public function mask($width, $frame, $level) + { + $minDemerit = PHP_INT_MAX; + $bestMaskNum = 0; + $bestMask = array(); + + $checked_masks = array(0,1,2,3,4,5,6,7); + + if (QR_FIND_FROM_RANDOM !== false) { + + $howManuOut = 8-(QR_FIND_FROM_RANDOM % 9); + for ($i = 0; $i < $howManuOut; $i++) { + $remPos = rand (0, count($checked_masks)-1); + unset($checked_masks[$remPos]); + $checked_masks = array_values($checked_masks); + } + + } + + $bestMask = $frame; + + foreach($checked_masks as $i) { + $mask = array_fill(0, $width, str_repeat("\0", $width)); + + $demerit = 0; + $blacks = 0; + $blacks = $this->makeMaskNo($i, $width, $frame, $mask); + $blacks += $this->writeFormatInformation($width, $mask, $i, $level); + $blacks = (int)(100 * $blacks / ($width * $width)); + $demerit = (int)((int)(abs($blacks - 50) / 5) * N4); + $demerit += $this->evaluateSymbol($width, $mask); + + if($demerit < $minDemerit) { + $minDemerit = $demerit; + $bestMask = $mask; + $bestMaskNum = $i; + } + } + + return $bestMask; + } + + //---------------------------------------------------------------------- + } diff --git a/application/libraries/phpqrcode/qrrscode.php b/application/libraries/phpqrcode/qrrscode.php new file mode 100644 index 0000000..591129a --- /dev/null +++ b/application/libraries/phpqrcode/qrrscode.php @@ -0,0 +1,210 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsItem { + + public $mm; // Bits per symbol + public $nn; // Symbols per block (= (1<= $this->nn) { + $x -= $this->nn; + $x = ($x >> $this->mm) + ($x & $this->nn); + } + + return $x; + } + + //---------------------------------------------------------------------- + public static function init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + // Common code for intializing a Reed-Solomon control block (char or int symbols) + // Copyright 2004 Phil Karn, KA9Q + // May be used under the terms of the GNU Lesser General Public License (LGPL) + + $rs = null; + + // Check parameter ranges + if($symsize < 0 || $symsize > 8) return $rs; + if($fcr < 0 || $fcr >= (1<<$symsize)) return $rs; + if($prim <= 0 || $prim >= (1<<$symsize)) return $rs; + if($nroots < 0 || $nroots >= (1<<$symsize)) return $rs; // Can't have more roots than symbol values! + if($pad < 0 || $pad >= ((1<<$symsize) -1 - $nroots)) return $rs; // Too much padding + + $rs = new QRrsItem(); + $rs->mm = $symsize; + $rs->nn = (1<<$symsize)-1; + $rs->pad = $pad; + + $rs->alpha_to = array_fill(0, $rs->nn+1, 0); + $rs->index_of = array_fill(0, $rs->nn+1, 0); + + // PHP style macro replacement ;) + $NN =& $rs->nn; + $A0 =& $NN; + + // Generate Galois field lookup tables + $rs->index_of[0] = $A0; // log(zero) = -inf + $rs->alpha_to[$A0] = 0; // alpha**-inf = 0 + $sr = 1; + + for($i=0; $i<$rs->nn; $i++) { + $rs->index_of[$sr] = $i; + $rs->alpha_to[$i] = $sr; + $sr <<= 1; + if($sr & (1<<$symsize)) { + $sr ^= $gfpoly; + } + $sr &= $rs->nn; + } + + if($sr != 1){ + // field generator polynomial is not primitive! + $rs = NULL; + return $rs; + } + + /* Form RS code generator polynomial from its roots */ + $rs->genpoly = array_fill(0, $nroots+1, 0); + + $rs->fcr = $fcr; + $rs->prim = $prim; + $rs->nroots = $nroots; + $rs->gfpoly = $gfpoly; + + /* Find prim-th root of 1, used in decoding */ + for($iprim=1;($iprim % $prim) != 0;$iprim += $rs->nn) + ; // intentional empty-body loop! + + $rs->iprim = (int)($iprim / $prim); + $rs->genpoly[0] = 1; + + for ($i = 0,$root=$fcr*$prim; $i < $nroots; $i++, $root += $prim) { + $rs->genpoly[$i+1] = 1; + + // Multiply rs->genpoly[] by @**(root + x) + for ($j = $i; $j > 0; $j--) { + if ($rs->genpoly[$j] != 0) { + $rs->genpoly[$j] = $rs->genpoly[$j-1] ^ $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[$j]] + $root)]; + } else { + $rs->genpoly[$j] = $rs->genpoly[$j-1]; + } + } + // rs->genpoly[0] can never be zero + $rs->genpoly[0] = $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[0]] + $root)]; + } + + // convert rs->genpoly[] to index form for quicker encoding + for ($i = 0; $i <= $nroots; $i++) + $rs->genpoly[$i] = $rs->index_of[$rs->genpoly[$i]]; + + return $rs; + } + + //---------------------------------------------------------------------- + public function encode_rs_char($data, &$parity) + { + $MM =& $this->mm; + $NN =& $this->nn; + $ALPHA_TO =& $this->alpha_to; + $INDEX_OF =& $this->index_of; + $GENPOLY =& $this->genpoly; + $NROOTS =& $this->nroots; + $FCR =& $this->fcr; + $PRIM =& $this->prim; + $IPRIM =& $this->iprim; + $PAD =& $this->pad; + $A0 =& $NN; + + $parity = array_fill(0, $NROOTS, 0); + + for($i=0; $i< ($NN-$NROOTS-$PAD); $i++) { + + $feedback = $INDEX_OF[$data[$i] ^ $parity[0]]; + if($feedback != $A0) { + // feedback term is non-zero + + // This line is unnecessary when GENPOLY[NROOTS] is unity, as it must + // always be for the polynomials constructed by init_rs() + $feedback = $this->modnn($NN - $GENPOLY[$NROOTS] + $feedback); + + for($j=1;$j<$NROOTS;$j++) { + $parity[$j] ^= $ALPHA_TO[$this->modnn($feedback + $GENPOLY[$NROOTS-$j])]; + } + } + + // Shift + array_shift($parity); + if($feedback != $A0) { + array_push($parity, $ALPHA_TO[$this->modnn($feedback + $GENPOLY[0])]); + } else { + array_push($parity, 0); + } + } + } + } + + //########################################################################## + + class QRrs { + + public static $items = array(); + + //---------------------------------------------------------------------- + public static function init_rs($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + foreach(self::$items as $rs) { + if($rs->pad != $pad) continue; + if($rs->nroots != $nroots) continue; + if($rs->mm != $symsize) continue; + if($rs->gfpoly != $gfpoly) continue; + if($rs->fcr != $fcr) continue; + if($rs->prim != $prim) continue; + + return $rs; + } + + $rs = QRrsItem::init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad); + array_unshift(self::$items, $rs); + + return $rs; + } + } \ No newline at end of file diff --git a/application/libraries/phpqrcode/qrspec.php b/application/libraries/phpqrcode/qrspec.php new file mode 100644 index 0000000..92aea0c --- /dev/null +++ b/application/libraries/phpqrcode/qrspec.php @@ -0,0 +1,592 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QRSPEC_VERSION_MAX', 40); + define('QRSPEC_WIDTH_MAX', 177); + + define('QRCAP_WIDTH', 0); + define('QRCAP_WORDS', 1); + define('QRCAP_REMINDER', 2); + define('QRCAP_EC', 3); + + class QRspec { + + public static $capacity = array( + array( 0, 0, 0, array( 0, 0, 0, 0)), + array( 21, 26, 0, array( 7, 10, 13, 17)), // 1 + array( 25, 44, 7, array( 10, 16, 22, 28)), + array( 29, 70, 7, array( 15, 26, 36, 44)), + array( 33, 100, 7, array( 20, 36, 52, 64)), + array( 37, 134, 7, array( 26, 48, 72, 88)), // 5 + array( 41, 172, 7, array( 36, 64, 96, 112)), + array( 45, 196, 0, array( 40, 72, 108, 130)), + array( 49, 242, 0, array( 48, 88, 132, 156)), + array( 53, 292, 0, array( 60, 110, 160, 192)), + array( 57, 346, 0, array( 72, 130, 192, 224)), //10 + array( 61, 404, 0, array( 80, 150, 224, 264)), + array( 65, 466, 0, array( 96, 176, 260, 308)), + array( 69, 532, 0, array( 104, 198, 288, 352)), + array( 73, 581, 3, array( 120, 216, 320, 384)), + array( 77, 655, 3, array( 132, 240, 360, 432)), //15 + array( 81, 733, 3, array( 144, 280, 408, 480)), + array( 85, 815, 3, array( 168, 308, 448, 532)), + array( 89, 901, 3, array( 180, 338, 504, 588)), + array( 93, 991, 3, array( 196, 364, 546, 650)), + array( 97, 1085, 3, array( 224, 416, 600, 700)), //20 + array(101, 1156, 4, array( 224, 442, 644, 750)), + array(105, 1258, 4, array( 252, 476, 690, 816)), + array(109, 1364, 4, array( 270, 504, 750, 900)), + array(113, 1474, 4, array( 300, 560, 810, 960)), + array(117, 1588, 4, array( 312, 588, 870, 1050)), //25 + array(121, 1706, 4, array( 336, 644, 952, 1110)), + array(125, 1828, 4, array( 360, 700, 1020, 1200)), + array(129, 1921, 3, array( 390, 728, 1050, 1260)), + array(133, 2051, 3, array( 420, 784, 1140, 1350)), + array(137, 2185, 3, array( 450, 812, 1200, 1440)), //30 + array(141, 2323, 3, array( 480, 868, 1290, 1530)), + array(145, 2465, 3, array( 510, 924, 1350, 1620)), + array(149, 2611, 3, array( 540, 980, 1440, 1710)), + array(153, 2761, 3, array( 570, 1036, 1530, 1800)), + array(157, 2876, 0, array( 570, 1064, 1590, 1890)), //35 + array(161, 3034, 0, array( 600, 1120, 1680, 1980)), + array(165, 3196, 0, array( 630, 1204, 1770, 2100)), + array(169, 3362, 0, array( 660, 1260, 1860, 2220)), + array(173, 3532, 0, array( 720, 1316, 1950, 2310)), + array(177, 3706, 0, array( 750, 1372, 2040, 2430)) //40 + ); + + //---------------------------------------------------------------------- + public static function getDataLength($version, $level) + { + return self::$capacity[$version][QRCAP_WORDS] - self::$capacity[$version][QRCAP_EC][$level]; + } + + //---------------------------------------------------------------------- + public static function getECCLength($version, $level) + { + return self::$capacity[$version][QRCAP_EC][$level]; + } + + //---------------------------------------------------------------------- + public static function getWidth($version) + { + return self::$capacity[$version][QRCAP_WIDTH]; + } + + //---------------------------------------------------------------------- + public static function getRemainder($version) + { + return self::$capacity[$version][QRCAP_REMINDER]; + } + + //---------------------------------------------------------------------- + public static function getMinimumVersion($size, $level) + { + + for($i=1; $i<= QRSPEC_VERSION_MAX; $i++) { + $words = self::$capacity[$i][QRCAP_WORDS] - self::$capacity[$i][QRCAP_EC][$level]; + if($words >= $size) + return $i; + } + + return -1; + } + + //###################################################################### + + public static $lengthTableBits = array( + array(10, 12, 14), + array( 9, 11, 13), + array( 8, 16, 16), + array( 8, 10, 12) + ); + + //---------------------------------------------------------------------- + public static function lengthIndicator($mode, $version) + { + if ($mode == QR_MODE_STRUCTURE) + return 0; + + if ($version <= 9) { + $l = 0; + } else if ($version <= 26) { + $l = 1; + } else { + $l = 2; + } + + return self::$lengthTableBits[$mode][$l]; + } + + //---------------------------------------------------------------------- + public static function maximumWords($mode, $version) + { + if($mode == QR_MODE_STRUCTURE) + return 3; + + if($version <= 9) { + $l = 0; + } else if($version <= 26) { + $l = 1; + } else { + $l = 2; + } + + $bits = self::$lengthTableBits[$mode][$l]; + $words = (1 << $bits) - 1; + + if($mode == QR_MODE_KANJI) { + $words *= 2; // the number of bytes is required + } + + return $words; + } + + // Error correction code ----------------------------------------------- + // Table of the error correction code (Reed-Solomon block) + // See Table 12-16 (pp.30-36), JIS X0510:2004. + + public static $eccTable = array( + array(array( 0, 0), array( 0, 0), array( 0, 0), array( 0, 0)), + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), // 1 + array(array( 1, 0), array( 1, 0), array( 1, 0), array( 1, 0)), + array(array( 1, 0), array( 1, 0), array( 2, 0), array( 2, 0)), + array(array( 1, 0), array( 2, 0), array( 2, 0), array( 4, 0)), + array(array( 1, 0), array( 2, 0), array( 2, 2), array( 2, 2)), // 5 + array(array( 2, 0), array( 4, 0), array( 4, 0), array( 4, 0)), + array(array( 2, 0), array( 4, 0), array( 2, 4), array( 4, 1)), + array(array( 2, 0), array( 2, 2), array( 4, 2), array( 4, 2)), + array(array( 2, 0), array( 3, 2), array( 4, 4), array( 4, 4)), + array(array( 2, 2), array( 4, 1), array( 6, 2), array( 6, 2)), //10 + array(array( 4, 0), array( 1, 4), array( 4, 4), array( 3, 8)), + array(array( 2, 2), array( 6, 2), array( 4, 6), array( 7, 4)), + array(array( 4, 0), array( 8, 1), array( 8, 4), array(12, 4)), + array(array( 3, 1), array( 4, 5), array(11, 5), array(11, 5)), + array(array( 5, 1), array( 5, 5), array( 5, 7), array(11, 7)), //15 + array(array( 5, 1), array( 7, 3), array(15, 2), array( 3, 13)), + array(array( 1, 5), array(10, 1), array( 1, 15), array( 2, 17)), + array(array( 5, 1), array( 9, 4), array(17, 1), array( 2, 19)), + array(array( 3, 4), array( 3, 11), array(17, 4), array( 9, 16)), + array(array( 3, 5), array( 3, 13), array(15, 5), array(15, 10)), //20 + array(array( 4, 4), array(17, 0), array(17, 6), array(19, 6)), + array(array( 2, 7), array(17, 0), array( 7, 16), array(34, 0)), + array(array( 4, 5), array( 4, 14), array(11, 14), array(16, 14)), + array(array( 6, 4), array( 6, 14), array(11, 16), array(30, 2)), + array(array( 8, 4), array( 8, 13), array( 7, 22), array(22, 13)), //25 + array(array(10, 2), array(19, 4), array(28, 6), array(33, 4)), + array(array( 8, 4), array(22, 3), array( 8, 26), array(12, 28)), + array(array( 3, 10), array( 3, 23), array( 4, 31), array(11, 31)), + array(array( 7, 7), array(21, 7), array( 1, 37), array(19, 26)), + array(array( 5, 10), array(19, 10), array(15, 25), array(23, 25)), //30 + array(array(13, 3), array( 2, 29), array(42, 1), array(23, 28)), + array(array(17, 0), array(10, 23), array(10, 35), array(19, 35)), + array(array(17, 1), array(14, 21), array(29, 19), array(11, 46)), + array(array(13, 6), array(14, 23), array(44, 7), array(59, 1)), + array(array(12, 7), array(12, 26), array(39, 14), array(22, 41)), //35 + array(array( 6, 14), array( 6, 34), array(46, 10), array( 2, 64)), + array(array(17, 4), array(29, 14), array(49, 10), array(24, 46)), + array(array( 4, 18), array(13, 32), array(48, 14), array(42, 32)), + array(array(20, 4), array(40, 7), array(43, 22), array(10, 67)), + array(array(19, 6), array(18, 31), array(34, 34), array(20, 61)),//40 + ); + + //---------------------------------------------------------------------- + // CACHEABLE!!! + + public static function getEccSpec($version, $level, array &$spec) + { + if (count($spec) < 5) { + $spec = array(0,0,0,0,0); + } + + $b1 = self::$eccTable[$version][$level][0]; + $b2 = self::$eccTable[$version][$level][1]; + $data = self::getDataLength($version, $level); + $ecc = self::getECCLength($version, $level); + + if($b2 == 0) { + $spec[0] = $b1; + $spec[1] = (int)($data / $b1); + $spec[2] = (int)($ecc / $b1); + $spec[3] = 0; + $spec[4] = 0; + } else { + $spec[0] = $b1; + $spec[1] = (int)($data / ($b1 + $b2)); + $spec[2] = (int)($ecc / ($b1 + $b2)); + $spec[3] = $b2; + $spec[4] = $spec[1] + 1; + } + } + + // Alignment pattern --------------------------------------------------- + + // Positions of alignment patterns. + // This array includes only the second and the third position of the + // alignment patterns. Rest of them can be calculated from the distance + // between them. + + // See Table 1 in Appendix E (pp.71) of JIS X0510:2004. + + public static $alignmentPattern = array( + array( 0, 0), + array( 0, 0), array(18, 0), array(22, 0), array(26, 0), array(30, 0), // 1- 5 + array(34, 0), array(22, 38), array(24, 42), array(26, 46), array(28, 50), // 6-10 + array(30, 54), array(32, 58), array(34, 62), array(26, 46), array(26, 48), //11-15 + array(26, 50), array(30, 54), array(30, 56), array(30, 58), array(34, 62), //16-20 + array(28, 50), array(26, 50), array(30, 54), array(28, 54), array(32, 58), //21-25 + array(30, 58), array(34, 62), array(26, 50), array(30, 54), array(26, 52), //26-30 + array(30, 56), array(34, 60), array(30, 58), array(34, 62), array(30, 54), //31-35 + array(24, 50), array(28, 54), array(32, 58), array(26, 54), array(30, 58), //35-40 + ); + + + /** -------------------------------------------------------------------- + * Put an alignment marker. + * @param frame + * @param width + * @param ox,oy center coordinate of the pattern + */ + public static function putAlignmentMarker(array &$frame, $ox, $oy) + { + $finder = array( + "\xa1\xa1\xa1\xa1\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa0\xa1\xa0\xa1", + "\xa1\xa0\xa0\xa0\xa1", + "\xa1\xa1\xa1\xa1\xa1" + ); + + $yStart = $oy-2; + $xStart = $ox-2; + + for($y=0; $y<5; $y++) { + QRstr::set($frame, $xStart, $yStart+$y, $finder[$y]); + } + } + + //---------------------------------------------------------------------- + public static function putAlignmentPattern($version, &$frame, $width) + { + if($version < 2) + return; + + $d = self::$alignmentPattern[$version][1] - self::$alignmentPattern[$version][0]; + if($d < 0) { + $w = 2; + } else { + $w = (int)(($width - self::$alignmentPattern[$version][0]) / $d + 2); + } + + if($w * $w - 3 == 1) { + $x = self::$alignmentPattern[$version][0]; + $y = self::$alignmentPattern[$version][0]; + self::putAlignmentMarker($frame, $x, $y); + return; + } + + $cx = self::$alignmentPattern[$version][0]; + for($x=1; $x<$w - 1; $x++) { + self::putAlignmentMarker($frame, 6, $cx); + self::putAlignmentMarker($frame, $cx, 6); + $cx += $d; + } + + $cy = self::$alignmentPattern[$version][0]; + for($y=0; $y<$w-1; $y++) { + $cx = self::$alignmentPattern[$version][0]; + for($x=0; $x<$w-1; $x++) { + self::putAlignmentMarker($frame, $cx, $cy); + $cx += $d; + } + $cy += $d; + } + } + + // Version information pattern ----------------------------------------- + + // Version information pattern (BCH coded). + // See Table 1 in Appendix D (pp.68) of JIS X0510:2004. + + // size: [QRSPEC_VERSION_MAX - 6] + + public static $versionPattern = array( + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, + 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, + 0x27541, 0x28c69 + ); + + //---------------------------------------------------------------------- + public static function getVersionPattern($version) + { + if($version < 7 || $version > QRSPEC_VERSION_MAX) + return 0; + + return self::$versionPattern[$version -7]; + } + + // Format information -------------------------------------------------- + // See calcFormatInfo in tests/test_qrspec.c (orginal qrencode c lib) + + public static $formatInfo = array( + array(0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976), + array(0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0), + array(0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed), + array(0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b) + ); + + public static function getFormatInfo($mask, $level) + { + if($mask < 0 || $mask > 7) + return 0; + + if($level < 0 || $level > 3) + return 0; + + return self::$formatInfo[$level][$mask]; + } + + // Frame --------------------------------------------------------------- + // Cache of initial frames. + + public static $frames = array(); + + /** -------------------------------------------------------------------- + * Put a finder pattern. + * @param frame + * @param width + * @param ox,oy upper-left coordinate of the pattern + */ + public static function putFinderPattern(&$frame, $ox, $oy) + { + $finder = array( + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc1\xc1\xc1\xc0\xc1", + "\xc1\xc0\xc0\xc0\xc0\xc0\xc1", + "\xc1\xc1\xc1\xc1\xc1\xc1\xc1" + ); + + for($y=0; $y<7; $y++) { + QRstr::set($frame, $ox, $oy+$y, $finder[$y]); + } + } + + //---------------------------------------------------------------------- + public static function createFrame($version) + { + $width = self::$capacity[$version][QRCAP_WIDTH]; + $frameLine = str_repeat ("\0", $width); + $frame = array_fill(0, $width, $frameLine); + + // Finder pattern + self::putFinderPattern($frame, 0, 0); + self::putFinderPattern($frame, $width - 7, 0); + self::putFinderPattern($frame, 0, $width - 7); + + // Separator + $yOffset = $width - 7; + + for($y=0; $y<7; $y++) { + $frame[$y][7] = "\xc0"; + $frame[$y][$width - 8] = "\xc0"; + $frame[$yOffset][7] = "\xc0"; + $yOffset++; + } + + $setPattern = str_repeat("\xc0", 8); + + QRstr::set($frame, 0, 7, $setPattern); + QRstr::set($frame, $width-8, 7, $setPattern); + QRstr::set($frame, 0, $width - 8, $setPattern); + + // Format info + $setPattern = str_repeat("\x84", 9); + QRstr::set($frame, 0, 8, $setPattern); + QRstr::set($frame, $width - 8, 8, $setPattern, 8); + + $yOffset = $width - 8; + + for($y=0; $y<8; $y++,$yOffset++) { + $frame[$y][8] = "\x84"; + $frame[$yOffset][8] = "\x84"; + } + + // Timing pattern + + for($i=1; $i<$width-15; $i++) { + $frame[6][7+$i] = chr(0x90 | ($i & 1)); + $frame[7+$i][6] = chr(0x90 | ($i & 1)); + } + + // Alignment pattern + self::putAlignmentPattern($version, $frame, $width); + + // Version information + if($version >= 7) { + $vinf = self::getVersionPattern($version); + + $v = $vinf; + + for($x=0; $x<6; $x++) { + for($y=0; $y<3; $y++) { + $frame[($width - 11)+$y][$x] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + + $v = $vinf; + for($y=0; $y<6; $y++) { + for($x=0; $x<3; $x++) { + $frame[$y][$x+($width - 11)] = chr(0x88 | ($v & 1)); + $v = $v >> 1; + } + } + } + + // and a little bit... + $frame[$width - 8][8] = "\x81"; + + return $frame; + } + + //---------------------------------------------------------------------- + public static function debug($frame, $binary_mode = false) + { + if ($binary_mode) { + + foreach ($frame as &$frameLine) { + $frameLine = join('  ', explode('0', $frameLine)); + $frameLine = join('██', explode('1', $frameLine)); + } + + ?> + +


        '; + echo join("
        ", $frame); + echo '






'; + + } else { + + foreach ($frame as &$frameLine) { + $frameLine = join(' ', explode("\xc0", $frameLine)); + $frameLine = join('▒', explode("\xc1", $frameLine)); + $frameLine = join(' ', explode("\xa0", $frameLine)); + $frameLine = join('▒', explode("\xa1", $frameLine)); + $frameLine = join('◇', explode("\x84", $frameLine)); //format 0 + $frameLine = join('◆', explode("\x85", $frameLine)); //format 1 + $frameLine = join('☢', explode("\x81", $frameLine)); //special bit + $frameLine = join(' ', explode("\x90", $frameLine)); //clock 0 + $frameLine = join('◷', explode("\x91", $frameLine)); //clock 1 + $frameLine = join(' ', explode("\x88", $frameLine)); //version + $frameLine = join('▒', explode("\x89", $frameLine)); //version + $frameLine = join('♦', explode("\x01", $frameLine)); + $frameLine = join('⋅', explode("\0", $frameLine)); + } + + ?> + + "; + echo join("
", $frame); + echo "
"; + + } + } + + //---------------------------------------------------------------------- + public static function serial($frame) + { + return gzcompress(join("\n", $frame), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + return explode("\n", gzuncompress($code)); + } + + //---------------------------------------------------------------------- + public static function newFrame($version) + { + if($version < 1 || $version > QRSPEC_VERSION_MAX) + return null; + + if(!isset(self::$frames[$version])) { + + $fileName = QR_CACHE_DIR.'frame_'.$version.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + self::$frames[$version] = self::unserial(file_get_contents($fileName)); + } else { + self::$frames[$version] = self::createFrame($version); + file_put_contents($fileName, self::serial(self::$frames[$version])); + } + } else { + self::$frames[$version] = self::createFrame($version); + } + } + + if(is_null(self::$frames[$version])) + return null; + + return self::$frames[$version]; + } + + //---------------------------------------------------------------------- + public static function rsBlockNum($spec) { return $spec[0] + $spec[3]; } + public static function rsBlockNum1($spec) { return $spec[0]; } + public static function rsDataCodes1($spec) { return $spec[1]; } + public static function rsEccCodes1($spec) { return $spec[2]; } + public static function rsBlockNum2($spec) { return $spec[3]; } + public static function rsDataCodes2($spec) { return $spec[4]; } + public static function rsEccCodes2($spec) { return $spec[2]; } + public static function rsDataLength($spec) { return ($spec[0] * $spec[1]) + ($spec[3] * $spec[4]); } + public static function rsEccLength($spec) { return ($spec[0] + $spec[3]) * $spec[2]; } + + } \ No newline at end of file diff --git a/application/libraries/phpqrcode/qrsplit.php b/application/libraries/phpqrcode/qrsplit.php new file mode 100644 index 0000000..d75b827 --- /dev/null +++ b/application/libraries/phpqrcode/qrsplit.php @@ -0,0 +1,311 @@ + + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + class QRsplit { + + public $dataStr = ''; + public $input; + public $modeHint; + + //---------------------------------------------------------------------- + public function __construct($dataStr, $input, $modeHint) + { + $this->dataStr = $dataStr; + $this->input = $input; + $this->modeHint = $modeHint; + } + + //---------------------------------------------------------------------- + public static function isdigitat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return ((ord($str[$pos]) >= ord('0'))&&(ord($str[$pos]) <= ord('9'))); + } + + //---------------------------------------------------------------------- + public static function isalnumat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return (QRinput::lookAnTable(ord($str[$pos])) >= 0); + } + + //---------------------------------------------------------------------- + public function identifyMode($pos) + { + if ($pos >= strlen($this->dataStr)) + return QR_MODE_NUL; + + $c = $this->dataStr[$pos]; + + if(self::isdigitat($this->dataStr, $pos)) { + return QR_MODE_NUM; + } else if(self::isalnumat($this->dataStr, $pos)) { + return QR_MODE_AN; + } else if($this->modeHint == QR_MODE_KANJI) { + + if ($pos+1 < strlen($this->dataStr)) + { + $d = $this->dataStr[$pos+1]; + $word = (ord($c) << 8) | ord($d); + if(($word >= 0x8140 && $word <= 0x9ffc) || ($word >= 0xe040 && $word <= 0xebbf)) { + return QR_MODE_KANJI; + } + } + } + + return QR_MODE_8; + } + + //---------------------------------------------------------------------- + public function eatNum() + { + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + while(self::isdigitat($this->dataStr, $p)) { + $p++; + } + + $run = $p; + $mode = $this->identifyMode($p); + + if($mode == QR_MODE_8) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + if($mode == QR_MODE_AN) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsModeAn(1) // + 4 + la + - QRinput::estimateBitsModeAn($run + 1);// - 4 - la + if($dif > 0) { + return $this->eatAn(); + } + } + + $ret = $this->input->append(QR_MODE_NUM, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatAn() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + + while(self::isalnumat($this->dataStr, $p)) { + if(self::isdigitat($this->dataStr, $p)) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + + $dif = QRinput::estimateBitsModeAn($p) // + 4 + la + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsModeAn($q); // - 4 - la + + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + + if(!self::isalnumat($this->dataStr, $p)) { + $dif = QRinput::estimateBitsModeAn($run) + 4 + $la + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + + $ret = $this->input->append(QR_MODE_AN, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatKanji() + { + $p = 0; + + while($this->identifyMode($p) == QR_MODE_KANJI) { + $p += 2; + } + + $ret = $this->input->append(QR_MODE_KANJI, $p, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eat8() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 1; + $dataStrLen = strlen($this->dataStr); + + while($p < $dataStrLen) { + + $mode = $this->identifyMode($p); + if($mode == QR_MODE_KANJI) { + break; + } + if($mode == QR_MODE_NUM) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else if($mode == QR_MODE_AN) { + $q = $p; + while(self::isalnumat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeAn($q - $p) + 4 + $la + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + $ret = $this->input->append(QR_MODE_8, $run, str_split($this->dataStr)); + + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function splitString() + { + while (strlen($this->dataStr) > 0) + { + if($this->dataStr == '') + return 0; + + $mode = $this->identifyMode(0); + + switch ($mode) { + case QR_MODE_NUM: $length = $this->eatNum(); break; + case QR_MODE_AN: $length = $this->eatAn(); break; + case QR_MODE_KANJI: + if ($hint == QR_MODE_KANJI) + $length = $this->eatKanji(); + else $length = $this->eat8(); + break; + default: $length = $this->eat8(); break; + + } + + if($length == 0) return 0; + if($length < 0) return -1; + + $this->dataStr = substr($this->dataStr, $length); + } + } + + //---------------------------------------------------------------------- + public function toUpper() + { + $stringLen = strlen($this->dataStr); + $p = 0; + + while ($p<$stringLen) { + $mode = self::identifyMode(substr($this->dataStr, $p), $this->modeHint); + if($mode == QR_MODE_KANJI) { + $p += 2; + } else { + if (ord($this->dataStr[$p]) >= ord('a') && ord($this->dataStr[$p]) <= ord('z')) { + $this->dataStr[$p] = chr(ord($this->dataStr[$p]) - 32); + } + $p++; + } + } + + return $this->dataStr; + } + + //---------------------------------------------------------------------- + public static function splitStringToQRinput($string, QRinput $input, $modeHint, $casesensitive = true) + { + if(is_null($string) || $string == '\0' || $string == '') { + throw new Exception('empty string!!!'); + } + + $split = new QRsplit($string, $input, $modeHint); + + if(!$casesensitive) + $split->toUpper(); + + return $split->splitString(); + } + } \ No newline at end of file diff --git a/application/libraries/phpqrcode/qrtools.php b/application/libraries/phpqrcode/qrtools.php new file mode 100644 index 0000000..3012db4 --- /dev/null +++ b/application/libraries/phpqrcode/qrtools.php @@ -0,0 +1,172 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRtools { + + //---------------------------------------------------------------------- + public static function binarize($frame) + { + $len = count($frame); + foreach ($frame as &$frameLine) { + + for($i=0; $i<$len; $i++) { + $frameLine[$i] = (ord($frameLine[$i])&1)?'1':'0'; + } + } + + return $frame; + } + + //---------------------------------------------------------------------- + public static function tcpdfBarcodeArray($code, $mode = 'QR,L', $tcPdfVersion = '4.5.037') + { + $barcode_array = array(); + + if (!is_array($mode)) + $mode = explode(',', $mode); + + $eccLevel = 'L'; + + if (count($mode) > 1) { + $eccLevel = $mode[1]; + } + + $qrTab = QRcode::text($code, false, $eccLevel); + $size = count($qrTab); + + $barcode_array['num_rows'] = $size; + $barcode_array['num_cols'] = $size; + $barcode_array['bcode'] = array(); + + foreach ($qrTab as $line) { + $arrAdd = array(); + foreach(str_split($line) as $char) + $arrAdd[] = ($char=='1')?1:0; + $barcode_array['bcode'][] = $arrAdd; + } + + return $barcode_array; + } + + //---------------------------------------------------------------------- + public static function clearCache() + { + self::$frames = array(); + } + + //---------------------------------------------------------------------- + public static function buildCache() + { + QRtools::markTime('before_build_cache'); + + $mask = new QRmask(); + for ($a=1; $a <= QRSPEC_VERSION_MAX; $a++) { + $frame = QRspec::newFrame($a); + if (QR_IMAGE) { + $fileName = QR_CACHE_DIR.'frame_'.$a.'.png'; + QRimage::png(self::binarize($frame), $fileName, 1, 0); + } + + $width = count($frame); + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + for ($maskNo=0; $maskNo<8; $maskNo++) + $mask->makeMaskNo($maskNo, $width, $frame, $bitMask, true); + } + + QRtools::markTime('after_build_cache'); + } + + //---------------------------------------------------------------------- + public static function log($outfile, $err) + { + if (QR_LOG_DIR !== false) { + if ($err != '') { + if ($outfile !== false) { + file_put_contents(QR_LOG_DIR.basename($outfile).'-errors.txt', date('Y-m-d H:i:s').': '.$err, FILE_APPEND); + } else { + file_put_contents(QR_LOG_DIR.'errors.txt', date('Y-m-d H:i:s').': '.$err, FILE_APPEND); + } + } + } + } + + //---------------------------------------------------------------------- + public static function dumpMask($frame) + { + $width = count($frame); + for($y=0;$y<$width;$y++) { + for($x=0;$x<$width;$x++) { + echo ord($frame[$y][$x]).','; + } + } + } + + //---------------------------------------------------------------------- + public static function markTime($markerId) + { + list($usec, $sec) = explode(" ", microtime()); + $time = ((float)$usec + (float)$sec); + + if (!isset($GLOBALS['qr_time_bench'])) + $GLOBALS['qr_time_bench'] = array(); + + $GLOBALS['qr_time_bench'][$markerId] = $time; + } + + //---------------------------------------------------------------------- + public static function timeBenchmark() + { + self::markTime('finish'); + + $lastTime = 0; + $startTime = 0; + $p = 0; + + echo ' + + '; + + foreach($GLOBALS['qr_time_bench'] as $markerId=>$thisTime) { + if ($p > 0) { + echo ''; + } else { + $startTime = $thisTime; + } + + $p++; + $lastTime = $thisTime; + } + + echo ' + + +
BENCHMARK
till '.$markerId.': '.number_format($thisTime-$lastTime, 6).'s
TOTAL: '.number_format($lastTime-$startTime, 6).'s
'; + } + + } + + //########################################################################## + + QRtools::markTime('start'); + \ No newline at end of file diff --git a/application/libraries/phpqrcode/tools/merge.bat b/application/libraries/phpqrcode/tools/merge.bat new file mode 100644 index 0000000..b60a485 --- /dev/null +++ b/application/libraries/phpqrcode/tools/merge.bat @@ -0,0 +1,2 @@ +php ./merge.php +pause \ No newline at end of file diff --git a/application/libraries/phpqrcode/tools/merge.php b/application/libraries/phpqrcode/tools/merge.php new file mode 100644 index 0000000..19d338b --- /dev/null +++ b/application/libraries/phpqrcode/tools/merge.php @@ -0,0 +1,70 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + $QR_BASEDIR = dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR; + $QR_TOOLSDIR = dirname(__FILE__).DIRECTORY_SEPARATOR; + + $outputFile = $QR_BASEDIR.'phpqrcode.php'; + + // Required libs + + $fileList = array( + $QR_BASEDIR.'qrconst.php', + $QR_TOOLSDIR.'merged_config.php', + $QR_BASEDIR.'qrtools.php', + $QR_BASEDIR.'qrspec.php', + $QR_BASEDIR.'qrimage.php', + $QR_BASEDIR.'qrinput.php', + $QR_BASEDIR.'qrbitstream.php', + $QR_BASEDIR.'qrsplit.php', + $QR_BASEDIR.'qrrscode.php', + $QR_BASEDIR.'qrmask.php', + $QR_BASEDIR.'qrencode.php' + ); + + $headerFile = $QR_TOOLSDIR.'merged_header.php'; + $versionFile = $QR_BASEDIR.'VERSION'; + + $outputCode = ''; + + foreach($fileList as $fileName) { + $outputCode .= "\n\n".'//---- '.basename($fileName).' -----------------------------'."\n\n"; + $anotherCode = file_get_contents($fileName); + $anotherCode = preg_replace ('/^<\?php/', '', $anotherCode); + $anotherCode = preg_replace ('/\?>\*$/', '', $anotherCode); + $outputCode .= "\n\n".$anotherCode."\n\n"; + } + + $versionDataEx = explode("\n", file_get_contents($versionFile)); + + $outputContents = file_get_contents($headerFile); + $outputContents .= "\n\n/*\n * Version: ".trim($versionDataEx[0])."\n * Build: ".trim($versionDataEx[1])."\n */\n\n"; + $outputContents .= $outputCode; + + file_put_contents($outputFile, $outputContents); + + \ No newline at end of file diff --git a/application/libraries/phpqrcode/tools/merge.sh b/application/libraries/phpqrcode/tools/merge.sh new file mode 100644 index 0000000..e4c2fbc --- /dev/null +++ b/application/libraries/phpqrcode/tools/merge.sh @@ -0,0 +1,2 @@ +#!/bin/sh +php ./merge.php \ No newline at end of file diff --git a/application/libraries/phpqrcode/tools/merged_config.php b/application/libraries/phpqrcode/tools/merged_config.php new file mode 100644 index 0000000..55ddb45 --- /dev/null +++ b/application/libraries/phpqrcode/tools/merged_config.php @@ -0,0 +1,17 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + \ No newline at end of file diff --git a/application/logs/index.html b/application/logs/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/logs/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/models/DynamicModel.php b/application/models/DynamicModel.php new file mode 100644 index 0000000..198454f --- /dev/null +++ b/application/models/DynamicModel.php @@ -0,0 +1,191 @@ +table = $table; + return $this; + } + + public function setColumn($order = [], $search = [], $default_order = []) + { + $this->column_order = $order; + $this->column_search = $search; + $this->order = $default_order; + return $this; + } + + // ========================= + // BASE QUERY (DATATABLE) + // ========================= + private function _get_datatables_query() + { + $this->db->from($this->table); + + // SEARCH + if (!empty($_POST['search']['value'])) { + $this->db->group_start(); + foreach ($this->column_search as $i => $item) { + if ($i === 0) { + $this->db->like($item, $_POST['search']['value']); + } else { + $this->db->or_like($item, $_POST['search']['value']); + } + } + $this->db->group_end(); + } + + // ORDER + if (isset($_POST['order'])) { + $col = $_POST['order'][0]['column']; + $dir = $_POST['order'][0]['dir']; + + if (isset($this->column_order[$col])) { + $this->db->order_by($this->column_order[$col], $dir); + } + } elseif (!empty($this->order)) { + foreach ($this->order as $key => $val) { + $this->db->order_by($key, $val); + } + } + } + + // ========================= + // DATATABLE GET + // ========================= + public function get_datatables() + { + $this->_get_datatables_query(); + + if ($_POST['length'] != -1) { + $this->db->limit($_POST['length'], $_POST['start']); + } + + return $this->db->get()->result(); + } + + public function count_filtered() + { + $this->_get_datatables_query(); + return $this->db->count_all_results(); + } + + public function count_all() + { + return $this->db->count_all($this->table); + } + + // ========================= + // CRUD BASIC + // ========================= + + public function get_all($orderBy = null, $direction = 'ASC') + { + if ($orderBy) { + $this->db->order_by($orderBy, $direction); + } + return $this->db->get($this->table)->result(); + } + + public function get_by_id($id, $pk = 'id') + { + return $this->db->where($pk, $id)->get($this->table)->row(); + } + + public function get_where($where = []) + { + return $this->db->where($where)->get($this->table)->result(); + } + + public function insert($data) + { + $this->db->insert($this->table, $data); + return $this->db->insert_id(); + } + + public function insert_batch($data) + { + return $this->db->insert_batch($this->table, $data); + } + + public function update($id, $data, $pk = 'id') + { + return $this->db->where($pk, $id)->update($this->table, $data); + } + + public function update_where($where, $data) + { + return $this->db->where($where)->update($this->table, $data); + } + + public function delete($id, $pk = 'id') + { + return $this->db->where($pk, $id)->delete($this->table); + } + + public function delete_where($where) + { + return $this->db->where($where)->delete($this->table); + } + + // ========================= + // ADVANCED QUERY + // ========================= + + public function join($table, $on, $type = 'left') + { + $this->db->join($table, $on, $type); + return $this; + } + + public function where($where) + { + $this->db->where($where); + return $this; + } + + public function like($field, $match) + { + $this->db->like($field, $match); + return $this; + } + + public function order_by($field, $direction = 'ASC') + { + $this->db->order_by($field, $direction); + return $this; + } + + public function limit($limit, $offset = 0) + { + $this->db->limit($limit, $offset); + return $this; + } + + public function get() + { + return $this->db->get($this->table)->result(); + } + + public function first() + { + return $this->db->get($this->table)->row(); + } + + public function query($sql, $binds = null) + { + return $this->db->query($sql, $binds)->result(); + } + + public function find($id, $pk = 'id') + { + return $this->db->where($pk, $id)->get($this->table)->row(); + } +} \ No newline at end of file diff --git a/application/models/SiswaModel.php b/application/models/SiswaModel.php new file mode 100644 index 0000000..25184a6 --- /dev/null +++ b/application/models/SiswaModel.php @@ -0,0 +1,39 @@ +db + ->order_by('nama_kelas', 'ASC') + ->order_by('nama', 'ASC') + ->get('v_siswa') + ->result(); + } + + // Insert siswa + public function insert($data) + { + return $this->db->insert('siswa', $data); + } + + // Cari ID kelas berdasarkan nama kelas + public function getKelasIdByName($nama) + { + $row = $this->db->get_where('kelas', ['nama' => $nama])->row(); + return $row ? $row->id : null; + } + + // Cari ID kamar berdasarkan nama kamar & nama gedung + public function getKamarIdByName($kamar, $gedung) + { + $row = $this->db->get_where('kamar', [ + 'nama' => $kamar, + 'gedung' => $gedung + ])->row(); + + return $row ? $row->id : null; + } +} diff --git a/application/models/index.html b/application/models/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/models/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/third_party/PhpSpreadsheet-5.3.0/CHANGELOG.md b/application/third_party/PhpSpreadsheet-5.3.0/CHANGELOG.md new file mode 100644 index 0000000..5419b4c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/CHANGELOG.md @@ -0,0 +1,2077 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com) +and this project adheres to [Semantic Versioning](https://semver.org). Thia is always true of the master branch. Some earlier branches remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version. + +## TBD - 5.4.0 + +### Added + +- Nothing yet. + +### Removed + +- Nothing yet. + +### Changed + +- Nothing yet. + +### Moved + +- Nothing yet. + +### Deprecated + +- Nothing yet. + +### Fixed + +- Nothing yet. + +## 2025-11-24 - 5.3.0 + +### Added + +- Formal support for Php8.5. +- Limited Printarea support for Html/Pdf. [Issue #3941](https://github.com/PHPOffice/PhpSpreadsheet/issues/3941) [PR #4711](https://github.com/PHPOffice/PhpSpreadsheet/pull/4711) +- Implement missing `INFO` function. [PR #4709](https://github.com/PHPOffice/PhpSpreadsheet/pull/4709) +- Implement missing `BAHTTEXT` function. [PR #4715](https://github.com/PHPOffice/PhpSpreadsheet/pull/4715) + +### Deprecated + +- $dataType, the second parameter of Cell::setValueExplicit, is currently optional. Omitting it is deprecated, and it will be required in a future release. + +### Fixed + +- Protected ranges and insert/delete rows/columns. [Issue #4695](https://github.com/PHPOffice/PhpSpreadsheet/issues/4695) [PR #4702](https://github.com/PHPOffice/PhpSpreadsheet/pull/4702) +- Unexpected Exception in Php DateTime. [Issue #4696](https://github.com/PHPOffice/PhpSpreadsheet/issues/4696) [Issue #917](https://github.com/PHPOffice/PhpSpreadsheet/issues/917) [PR #4697](https://github.com/PHPOffice/PhpSpreadsheet/pull/4697) +- Add missing Dutch translation to translation file. [PR #4707](https://github.com/PHPOffice/PhpSpreadsheet/pull/4707) +- Fix lots of typos throughout codebase. [PR #4705](https://github.com/PHPOffice/PhpSpreadsheet/pull/4705) +- Apply small code style improvements. [PR #4708](https://github.com/PHPOffice/PhpSpreadsheet/pull/4708) + +## 2025-10-25 - 5.2.0 + +### Added + +- This release should be usable with Php8.5 Release Candidates without deprecation messages. +- Option to display numbers with less precision. [Issue #4626](https://github.com/PHPOffice/PhpSpreadsheet/issues/4626) [PR #4640](https://github.com/PHPOffice/PhpSpreadsheet/pull/4640) +- Offer Tcpdf Interface which throws exception rather than die. [PR #4666](https://github.com/PHPOffice/PhpSpreadsheet/pull/4666) +- Xls Reader ListWorksheetDimensions method. [PR #4689](https://github.com/PHPOffice/PhpSpreadsheet/pull/4689) + +### Deprecated + +- Worksheet::getHashInt serves no useful purpose. No replacement. +- Spreadsheet::getId serves no useful purpose. No replacement. + +### Fixed + +- Performance improvement when working with large amounts of cells. [Issue #4607](https://github.com/PHPOffice/PhpSpreadsheet/issues/4607) [PR #4609](https://github.com/PHPOffice/PhpSpreadsheet/pull/4609) +- Minor improvements to Calculation coverage. [PR #4624](https://github.com/PHPOffice/PhpSpreadsheet/pull/4624) +- Conditional formatting in extLst. [Issue #4629](https://github.com/PHPOffice/PhpSpreadsheet/issues/4629) [PR #4633](https://github.com/PHPOffice/PhpSpreadsheet/pull/4633) +- Php8.5 deprecates use of null as array index. [PR #4634](https://github.com/PHPOffice/PhpSpreadsheet/pull/4634) +- Wrapped cells and default row height. [Issue #4584](https://github.com/PHPOffice/PhpSpreadsheet/issues/4584) [PR #4645](https://github.com/PHPOffice/PhpSpreadsheet/pull/4645) +- For Php8.5, replace one of our two uses of `__wakeup` with `__unserialize`, and eliminate the other. [PR #4639](https://github.com/PHPOffice/PhpSpreadsheet/pull/4639) +- Use prefix _xlfn for BASE function. [Issue #4638](https://github.com/PHPOffice/PhpSpreadsheet/issues/4638) [PR #4641](https://github.com/PHPOffice/PhpSpreadsheet/pull/4641) +- Warning messages with corrupt Xls file. [Issue #4647](https://github.com/PHPOffice/PhpSpreadsheet/issues/4647) [PR #4648](https://github.com/PHPOffice/PhpSpreadsheet/pull/4648) +- Additional support for union and intersection. [PR #4596](https://github.com/PHPOffice/PhpSpreadsheet/pull/4596) +- Missing array keys x,o,v for Xml Reader. [Issue #4668](https://github.com/PHPOffice/PhpSpreadsheet/issues/4668) [PR #4669](https://github.com/PHPOffice/PhpSpreadsheet/pull/4669) +- Missing array key x for Xlsx Reader VML. [Issue #4505](https://github.com/PHPOffice/PhpSpreadsheet/issues/4505) [PR #4676](https://github.com/PHPOffice/PhpSpreadsheet/pull/4676) +- Better support for Style Alignment Read Order. [Issue #850](https://github.com/PHPOffice/PhpSpreadsheet/issues/850) [PR #4655](https://github.com/PHPOffice/PhpSpreadsheet/pull/4655) +- More sophisticated workbook password algorithms (Xlsx only). [Issue #4673](https://github.com/PHPOffice/PhpSpreadsheet/issues/4673) [PR #4675](https://github.com/PHPOffice/PhpSpreadsheet/pull/4675) +- Xls Writer fix DIMENSIONS record. [Issue #4682](https://github.com/PHPOffice/PhpSpreadsheet/issues/4682) [PR #4687](https://github.com/PHPOffice/PhpSpreadsheet/pull/4687) +- Xls Reader listWorksheetInfo process MULRK records. [PR #4689](https://github.com/PHPOffice/PhpSpreadsheet/pull/4689) +- Make Reader Xls Escher generic. [PR #4690](https://github.com/PHPOffice/PhpSpreadsheet/pull/4690) + +## 2025-09-03 - 5.1.0 + +### Added + +- Add Conditional Formatting with IconSet (Xlsx only). [Issue #4560](https://github.com/PHPOffice/PhpSpreadsheet/issues/4560) [PR #4574](https://github.com/PHPOffice/PhpSpreadsheet/pull/4574) +- Copy cell adjusting formula. [Issue #1203](https://github.com/PHPOffice/PhpSpreadsheet/issues/1203) [PR #4577](https://github.com/PHPOffice/PhpSpreadsheet/pull/4577) +- splitRange and ProtectedRange. [Issue #1457](https://github.com/PHPOffice/PhpSpreadsheet/issues/1457) [PR #4580](https://github.com/PHPOffice/PhpSpreadsheet/pull/4580) +- Option to create Blank Sheet if LoadSheetsOnly doesn't find any. [PR #4618](https://github.com/PHPOffice/PhpSpreadsheet/pull/4618) + +### Fixed + +- Google-only formulas exported from Google Sheets. [Issue #1637](https://github.com/PHPOffice/PhpSpreadsheet/issues/1637) [PR #4579](https://github.com/PHPOffice/PhpSpreadsheet/pull/4579) +- Maximum column width. [PR #4581](https://github.com/PHPOffice/PhpSpreadsheet/pull/4581) +- PrintArea after row/column delete. [Issue #2912](https://github.com/PHPOffice/PhpSpreadsheet/issues/2912) [PR #4598](https://github.com/PHPOffice/PhpSpreadsheet/pull/4598) +- Remove deprecated imagedestroy call. [PR #4625](https://github.com/PHPOffice/PhpSpreadsheet/pull/4625) +- Excel 2007 problem with newlines. [Issue #4619](https://github.com/PHPOffice/PhpSpreadsheet/issues/4619) [PR #4620](https://github.com/PHPOffice/PhpSpreadsheet/pull/4620) +- Compatibility changes for Php 8.5. [PR #4601](https://github.com/PHPOffice/PhpSpreadsheet/pull/4601) [PR #4611](https://github.com/PHPOffice/PhpSpreadsheet/pull/4611) + +## 2025-08-10 - 5.0.0 + +### Breaking Changes + +- Images will be loaded from an external source (e.g. http://example.com/img.png) only if the reader is explicitly set to allow it via `$reader->setAllowExternalImages(true)`. We do not believe that loading of external images is a widely used feature. +- Deletion of items deprecated in Release 4. See "removed" below. +- Move some properties from Base Reader to Html Reader. [PR #4551](https://github.com/PHPOffice/PhpSpreadsheet/pull/4551) +- DefaultValueBinder will treat integers with more than 15 digits as strings. [Issue #4522](https://github.com/PHPOffice/PhpSpreadsheet/issues/4522) [PR #4527](https://github.com/PHPOffice/PhpSpreadsheet/pull/4527) + +### Removed + +- Theme public constants COLOR_SCHEME_2013_PLUS_NAME (use COLOR_SCHEME_2013_2022_NAME) and COLOR_SCHEME_2013_PLUS (use COLOR_SCHEME_2013_2022). + +### Fixed + +- Additional floating-point precision changes. [Issue #1324](https://github.com/PHPOffice/PhpSpreadsheet/issues/1324) [PR #4575](https://github.com/PHPOffice/PhpSpreadsheet/pull/4575) +- Header/Footer images expand location. [Issue #484](https://github.com/PHPOffice/PhpSpreadsheet/issues/484) [Issue #1318](https://github.com/PHPOffice/PhpSpreadsheet/issues/1318) [PR #4572](https://github.com/PHPOffice/PhpSpreadsheet/pull/4572) +- Create uninitialized cell if used in calculation. [Issue #4558](https://github.com/PHPOffice/PhpSpreadsheet/issues/4558) [Issue #4530](https://github.com/PHPOffice/PhpSpreadsheet/issues/4530) [PR #4565](https://github.com/PHPOffice/PhpSpreadsheet/pull/4565) +- Shared/Date::isDateTime handle cells which calculate as arrays. [Issue #4557](https://github.com/PHPOffice/PhpSpreadsheet/issues/4557) [PR #4562](https://github.com/PHPOffice/PhpSpreadsheet/pull/4562) +- Xlsx Writer eliminate xml:space from non-text nodes. [Issue #4542](https://github.com/PHPOffice/PhpSpreadsheet/issues/4542) [PR #4556](https://github.com/PHPOffice/PhpSpreadsheet/pull/4556) + +## 2025-07-23 - 4.5.0 + +### Added + +- Add to all readers the option to allow or forbid fetching external images. This is unconditionally allowed now. The default will be set to "allow", so no code changes are necessary. However, we are giving consideration to changing the default. [PR #4543](https://github.com/PHPOffice/PhpSpreadsheet/pull/4543) +- Address Excel Inappropriate Number Format Substitution. [PR #4532](https://github.com/PHPOffice/PhpSpreadsheet/pull/4532) + +### Fixed + +- Html Writer Conditional Formatting Inline Css. [Issue #4539](https://github.com/PHPOffice/PhpSpreadsheet/issues/4539) [PR #4541](https://github.com/PHPOffice/PhpSpreadsheet/pull/4541) +- Do not use htmlspecialchars when formatting XML. [Issue #4537](https://github.com/PHPOffice/PhpSpreadsheet/issues/4537) [PR #4540](https://github.com/PHPOffice/PhpSpreadsheet/pull/4540) +- Writer Html/Pdf support RTL alignment of tables. [Issue #1104](https://github.com/PHPOffice/PhpSpreadsheet/issues/1104) [PR #4535](https://github.com/PHPOffice/PhpSpreadsheet/pull/4535) +- Xlsx Reader use dynamic arrays if spreadsheet did so. [PR #4533](https://github.com/PHPOffice/PhpSpreadsheet/pull/4533) +- Ods Reader Nested table-row. [Issue #4528](https://github.com/PHPOffice/PhpSpreadsheet/issues/4528) [Issue #2507](https://github.com/PHPOffice/PhpSpreadsheet/issues/2507) [PR #4531](https://github.com/PHPOffice/PhpSpreadsheet/pull/4531) +- Recognize application/x-empty mimetype. [Issue #4521](https://github.com/PHPOffice/PhpSpreadsheet/issues/4521) [PR #4524](https://github.com/PHPOffice/PhpSpreadsheet/pull/4524) +- Micro-optimization in getSheetByName. [PR #4499](https://github.com/PHPOffice/PhpSpreadsheet/pull/4499) +- Bug in resizeMatricesExtend. [Issue #4451](https://github.com/PHPOffice/PhpSpreadsheet/issues/4451) [PR #4474](https://github.com/PHPOffice/PhpSpreadsheet/pull/4474) +- Allow Replace of Dummy Function with Custom Function. [PR #4544](https://github.com/PHPOffice/PhpSpreadsheet/pull/4544) +- Preserve 0x0a in Strings if Desired. [Issue #347](https://github.com/PHPOffice/PhpSpreadsheet/issues/347) [PR #4536](https://github.com/PHPOffice/PhpSpreadsheet/pull/4536) + +## 2025-06-22 - 4.4.0 + +### Added + +- VSTACK and HSTACK. [Issue #4485](https://github.com/PHPOffice/PhpSpreadsheet/issues/4485) [PR #4492](https://github.com/PHPOffice/PhpSpreadsheet/pull/4492) +- TOCOL and TOROW. [PR #4493](https://github.com/PHPOffice/PhpSpreadsheet/pull/4493) +- Support Current Office Theme. [PR #4500](https://github.com/PHPOffice/PhpSpreadsheet/pull/4500) + +### Deprecated + +- Theme constants COLOR_SCHEME_2013_PLUS_NAME (use COLOR_SCHEME_2013_2022_NAME) and COLOR_SCHEME_2013_PLUS (use COLOR_SCHEME_2013_2022). + +### Fixed + +- Various Writers RichText TextElement Should Inherit Cell Style. [Issue #1154](https://github.com/PHPOffice/PhpSpreadsheet/issues/1154) [PR #4487](https://github.com/PHPOffice/PhpSpreadsheet/pull/4487) +- Minor Changes to FILTER function. [PR #4491](https://github.com/PHPOffice/PhpSpreadsheet/pull/4491) +- Allow Xlsx Reader/Writer to support Font Charset. [Issue #2760](https://github.com/PHPOffice/PhpSpreadsheet/issues/2760) [PR #4501](https://github.com/PHPOffice/PhpSpreadsheet/pull/4501) +- AutoColor for LibreOffice Dark Mode [Discussion 4502](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4502) [PR #4503](https://github.com/PHPOffice/PhpSpreadsheet/pull/4503) +- Xlsx Style Writer Minor Refactoring. [PR #4508](https://github.com/PHPOffice/PhpSpreadsheet/pull/4508) +- Allow Xlsx Reader to Specify ParseHuge. [Issue #4260](https://github.com/PHPOffice/PhpSpreadsheet/issues/4260) [PR #4515](https://github.com/PHPOffice/PhpSpreadsheet/pull/4515) + +## 2025-05-26 - 4.3.1 + +### Fixed + +- Regression in Date::stringToExcel. [Issue #4488](https://github.com/PHPOffice/PhpSpreadsheet/issues/4488) [PR #4489](https://github.com/PHPOffice/PhpSpreadsheet/pull/4489) + +## 2025-05-25 - 4.3.0 + +### Added + +- Xml Reader recognize indents. [Issue #4448](https://github.com/PHPOffice/PhpSpreadsheet/issues/4448) [PR #4449](https://github.com/PHPOffice/PhpSpreadsheet/pull/4449) + +### Changed + +- Phpstan Level 10. + +### Fixed + +- Micro-optimization for excelToDateTimeObject. [Issue #4438](https://github.com/PHPOffice/PhpSpreadsheet/issues/4438) [PR #4442](https://github.com/PHPOffice/PhpSpreadsheet/pull/4442) +- Removing Columns/Rows Containing Merged Cells. [Issue #282](https://github.com/PHPOffice/PhpSpreadsheet/issues/282) [PR #4465](https://github.com/PHPOffice/PhpSpreadsheet/pull/4465) +- Print Area and Row Break. [Issue #1275](https://github.com/PHPOffice/PhpSpreadsheet/issues/1275) [PR #4450](https://github.com/PHPOffice/PhpSpreadsheet/pull/4450) +- Copy Styles after insertNewColumnBefore. [Issue #1425](https://github.com/PHPOffice/PhpSpreadsheet/issues/1425) [PR #4468](https://github.com/PHPOffice/PhpSpreadsheet/pull/4468) +- Xls Writer Treat Hyperlink Starting with # as Internal. [Issue #56](https://github.com/PHPOffice/PhpSpreadsheet/issues/56) [PR #4453](https://github.com/PHPOffice/PhpSpreadsheet/pull/4453) +- More Precision for Float to String Casts. [Issue #3899](https://github.com/PHPOffice/PhpSpreadsheet/issues/3899) [PR #4479](https://github.com/PHPOffice/PhpSpreadsheet/pull/4479) +- Hyperlink Styles. [Issue #1632](https://github.com/PHPOffice/PhpSpreadsheet/issues/1632) [PR #4478](https://github.com/PHPOffice/PhpSpreadsheet/pull/4478) +- ODS Handling of Ceiling and Floor. [Issue #477](https://github.com/PHPOffice/PhpSpreadsheet/issues/407) [PR #4466](https://github.com/PHPOffice/PhpSpreadsheet/pull/4466) +- Xlsx Reader Do Not Process Printer Settings for Dataonly. [Issue #4477](https://github.com/PHPOffice/PhpSpreadsheet/issues/4477) [PR #4480](https://github.com/PHPOffice/PhpSpreadsheet/pull/4480) + +## 2025-04-16 - 4.2.0 + +### Added + +- Add ability to add custom functions to Calculation. [PR #4390](https://github.com/PHPOffice/PhpSpreadsheet/pull/4390) +- Add FormulaRange to IgnoredErrors. [PR #4393](https://github.com/PHPOffice/PhpSpreadsheet/pull/4393) +- TextGrid improvements. [PR #4418](https://github.com/PHPOffice/PhpSpreadsheet/pull/4418) +- Permit read to class which extends Spreadsheet. [Discussion #4402](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4402) [PR #4404](https://github.com/PHPOffice/PhpSpreadsheet/pull/4404) +- Conditional and table formatting support for html writer [PR #4412](https://github.com/PHPOffice/PhpSpreadsheet/pull/4412) + +### Changed + +- Phpstan Version 2. [PR #4384](https://github.com/PHPOffice/PhpSpreadsheet/pull/4384) +- Start migration to Phpstan level 9. [PR #4396](https://github.com/PHPOffice/PhpSpreadsheet/pull/4396) +- Calculation locale logic moved to separate class. [PR #4398](https://github.com/PHPOffice/PhpSpreadsheet/pull/4398) +- TREND_POLYNOMIAL_* and TREND_BEST_FIT do not work, and are changed to throw Exceptions if attempted. (TREND_BEST_FIT_NO_POLY works.) An attempt to use an unknown trend type will now also throw an exception. [Issue #4400](https://github.com/PHPOffice/PhpSpreadsheet/issues/4400) [PR #4339](https://github.com/PHPOffice/PhpSpreadsheet/pull/4339) +- Month parameter of DATE function will now return VALUE if an ordinal string (e.g. '3rd') is used, but will accept bool or null. [PR #4420](https://github.com/PHPOffice/PhpSpreadsheet/pull/4420) + +### Fixed + +- Ignore fractional part of Drawing Shadow Alpha. [Issue #4415](https://github.com/PHPOffice/PhpSpreadsheet/issues/4415) [PR #4417](https://github.com/PHPOffice/PhpSpreadsheet/pull/4417) +- BIN2DEC, OCT2DEC, and HEX2DEC return numbers rather than strings. [Issue #4383](https://github.com/PHPOffice/PhpSpreadsheet/issues/4383) [PR #4389](https://github.com/PHPOffice/PhpSpreadsheet/pull/4389) +- Fix TREND_BEST_FIT_NO_POLY. [Issue #4400](https://github.com/PHPOffice/PhpSpreadsheet/issues/4400) [PR #4339](https://github.com/PHPOffice/PhpSpreadsheet/pull/4339) +- Ods Reader No DataType for Null Value. [Issue #4435](https://github.com/PHPOffice/PhpSpreadsheet/issues/4435) [PR #4436](https://github.com/PHPOffice/PhpSpreadsheet/pull/4436) +- Column widths not preserved when using read filter. [Issue #4416](https://github.com/PHPOffice/PhpSpreadsheet/issues/4416) [PR #4423](https://github.com/PHPOffice/PhpSpreadsheet/pull/4423) +- Fix typo in Style exportArray quotePrefix. [Issue #4422](https://github.com/PHPOffice/PhpSpreadsheet/issues/4422) [PR #4424](https://github.com/PHPOffice/PhpSpreadsheet/pull/4424) +- Tweak Spreadsheet clone. [PR #4419](https://github.com/PHPOffice/PhpSpreadsheet/pull/4419) +- Better handling of Chart DisplayBlanksAs. [Issue #4411](https://github.com/PHPOffice/PhpSpreadsheet/issues/4411) [PR #4414](https://github.com/PHPOffice/PhpSpreadsheet/pull/4414) + +## 2025-03-02 - 4.1.0 + +### Added + +- Support Justify Last Line. [Issue #4374](https://github.com/PHPOffice/PhpSpreadsheet/issues/4374) [PR #4373](https://github.com/PHPOffice/PhpSpreadsheet/pull/4373) +- Allow Spreadsheet clone. [PR #4370](https://github.com/PHPOffice/PhpSpreadsheet/pull/4370) + +### Changed + +- ListWorksheetInfo will now return sheetState (visible, hidden, veryHidden). [Issue #4345](https://github.com/PHPOffice/PhpSpreadsheet/issues/4345) [PR #4366](https://github.com/PHPOffice/PhpSpreadsheet/pull/4366) +- Start migration to Phpstan 2. [PR #4359](https://github.com/PHPOffice/PhpSpreadsheet/pull/4359) +- IOFactory identify can return, and createReader and CreateWriter can accept, a class name rather than a file type. [Issue #4357](https://github.com/PHPOffice/PhpSpreadsheet/issues/4357) [PR #4361](https://github.com/PHPOffice/PhpSpreadsheet/pull/4361) + +### Fixed + +- Refactor Helper/Html. [PR #4359](https://github.com/PHPOffice/PhpSpreadsheet/pull/4359) +- Handle #REF! as Argument to AVERAGEIF/COUNTIF/SUMIF. [Issue #4381](https://github.com/PHPOffice/PhpSpreadsheet/issues/4381) [PR #4382](https://github.com/PHPOffice/PhpSpreadsheet/pull/4382) +- Ignore ignoredErrors when not applicable. [Issue #4375](https://github.com/PHPOffice/PhpSpreadsheet/issues/4375) [PR #4377](https://github.com/PHPOffice/PhpSpreadsheet/pull/4377) +- Better handling of defined names on sheets whose titles include apostrophes. [Issue #4356](https://github.com/PHPOffice/PhpSpreadsheet/issues/4356) [Issue #4362](https://github.com/PHPOffice/PhpSpreadsheet/issues/4362) [Issue #4376](https://github.com/PHPOffice/PhpSpreadsheet/issues/4376) [PR #4360](https://github.com/PHPOffice/PhpSpreadsheet/pull/4360) +- Partial solution for removing rows or columns that include edge ranges. [Issue #1449](https://github.com/PHPOffice/PhpSpreadsheet/issues/1449) [PR #3528](https://github.com/PHPOffice/PhpSpreadsheet/pull/3528) +- Prefer mb_str_split to str_split. [PR #3341](https://github.com/PHPOffice/PhpSpreadsheet/pull/3341) + +## 2025-02-08 - 4.0.0 + +### BREAKING CHANGES + +- Data Validations will be stored by worksheet, not cell. Index can be one or more cells or cell ranges. [Issue #797](https://github.com/PHPOffice/PhpSpreadsheet/issues/797) [Issue #4091](https://github.com/PHPOffice/PhpSpreadsheet/issues/4091) [Issue #4206](https://github.com/PHPOffice/PhpSpreadsheet/issues/4206) [PR #4240](https://github.com/PHPOffice/PhpSpreadsheet/pull/4240) +- Conditional Formatting adds Priority property and handles overlapping ranges better. [Issue #4312](https://github.com/PHPOffice/PhpSpreadsheet/issues/4312) [Issue #4318](https://github.com/PHPOffice/PhpSpreadsheet/issues/4318) [PR #4314](https://github.com/PHPOffice/PhpSpreadsheet/pull/4314) +- Csv Reader will no longer auto-detect Mac line endings by default. Prior behavior can be explicitly enabled via `setTestAutoDetect(true)`, and it will not be possible at all with Php9+. [Issue #4092](https://github.com/PHPOffice/PhpSpreadsheet/issues/4092) [PR #4340](https://github.com/PHPOffice/PhpSpreadsheet/pull/4340) +- Html Writer will now use "better boolean" logic. Booleans will now be output by default as TRUE/FALSE rather than 1/null-string. Prior behavior can be explicitly enabled via `setBetterBoolean(false)`. [PR #4340](https://github.com/PHPOffice/PhpSpreadsheet/pull/4340) +- Xlsx Writer will now use false as the default for `forceFullCalc`. This affects writes with `preCalculateFormulas` set to false. Prior behavior can be explicitly enabled via `setForceFullCalc(null)`.[PR #4340](https://github.com/PHPOffice/PhpSpreadsheet/pull/4340) +- Deletion of items deprecated in Release 3. See "removed" below. + +### Added + +- Pdf Charts and Drawings. [Discussion #4129](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4129) [Discussion #4168](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4168) [PR #4327](https://github.com/PHPOffice/PhpSpreadsheet/pull/4327) +- Allow spreadsheet serialization. [Discussion #4324](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4324) [Issue #1741](https://github.com/PHPOffice/PhpSpreadsheet/issues/1741) [Issue #1757](https://github.com/PHPOffice/PhpSpreadsheet/issues/1757) [PR #4326](https://github.com/PHPOffice/PhpSpreadsheet/pull/4326) + +### Removed + +- Worksheet::getStyles - no replacement. [PR #4330](https://github.com/PHPOffice/PhpSpreadsheet/pull/4330) +- The following items were deprecated in release 3 and are now removed. +- Drawing::setIsUrl - no replacement. +- Settings::setLibXmlLoaderOptions() and Settings::getLibXmlLoaderOptions() - no replacement. +- Worksheet::getHashCode - no replacement. +- IReader::SKIP_EMPTY_CELLS - use its alias IGNORE_EMPTY_CELLS instead. +- Worksheet::getProtectedCells - use getProtectedCellRanges instead. +- Writer/Html::isMpdf property - use instanceof Mpdf instead. + +### Changed + +- Nothing yet. + +### Moved + +- Nothing yet. + +### Deprecated + +- Nothing yet. + +### Fixed + +- Xls writer Parser Mishandling True/False Argument. [Issue #4331](https://github.com/PHPOffice/PhpSpreadsheet/issues/4331) [PR #4333](https://github.com/PHPOffice/PhpSpreadsheet/pull/4333) +- Xls writer Parser Parse By Character Not Byte. [PR #4344](https://github.com/PHPOffice/PhpSpreadsheet/pull/4344) +- Minor changes to dynamic array calculations exposed by using explicit array return types in some tests. [PR #4328](https://github.com/PHPOffice/PhpSpreadsheet/pull/4328) + +## 2025-01-26 - 3.9.0 + +### Added + +- Methods to get style for row or column. [PR #4317](https://github.com/PHPOffice/PhpSpreadsheet/pull/4317) +- Method for duplicating worksheet in spreadsheet. [PR #4315](https://github.com/PHPOffice/PhpSpreadsheet/pull/4315) + +### Fixed + +- Security patch for control characters in protocol. +- Ods Reader Sheet Names with Period. [Issue #4311](https://github.com/PHPOffice/PhpSpreadsheet/issues/4311) [PR #4313](https://github.com/PHPOffice/PhpSpreadsheet/pull/4313) +- Mpdf and Tcpdf Hidden Columns and Merged Cells. [Issue #4319](https://github.com/PHPOffice/PhpSpreadsheet/issues/4319) [PR #4320](https://github.com/PHPOffice/PhpSpreadsheet/pull/4320) +- Html Writer Allow mailto. [Issue #4316](https://github.com/PHPOffice/PhpSpreadsheet/issues/4316) [PR #4322](https://github.com/PHPOffice/PhpSpreadsheet/pull/4322) +- Use composer/pcre rather than preg_* in Writer. [PR #4323](https://github.com/PHPOffice/PhpSpreadsheet/pull/4323) + +## 2025-01-11 - 3.8.0 + +### Added + +- CHOOSECOLS, CHOOSEROWS, DROP, TAKE, and EXPAND. [PR #4286](https://github.com/PHPOffice/PhpSpreadsheet/pull/4286) + +### Fixed + +- Security patch for Html navigation. +- Xlsx Reader Shared Formula with Boolean Result. Partial solution for [Issue #4280](https://github.com/PHPOffice/PhpSpreadsheet/issues/4280) [PR #4281](https://github.com/PHPOffice/PhpSpreadsheet/pull/4281) +- Retitling cloned Worksheets. [Issue #641](https://github.com/PHPOffice/PhpSpreadsheet/issues/641) [PR #4302](https://github.com/PHPOffice/PhpSpreadsheet/pull/4302) +- Extremely limited support for GROUPBY function. Partial response to [Issue #4282](https://github.com/PHPOffice/PhpSpreadsheet/issues/4282) [PR #4283](https://github.com/PHPOffice/PhpSpreadsheet/pull/4283) + +## 2024-12-26 - 3.7.0 + +### Deprecated + +- Drawing::setIsUrl is unneeded. The property is set when setPath determines whether path is a url. + +### Fixed + +- Security patches for Samples. +- Security patches for Html Writer. +- Avoid unexpected charset in currency symbol. [PR #4279](https://github.com/PHPOffice/PhpSpreadsheet/pull/4279) +- Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271) +- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Coverage-related tweaks to Xls Reader. [PR #4277](https://github.com/PHPOffice/PhpSpreadsheet/pull/4277) +- Several fixed to ODS Writer. [Issue #4261](https://github.com/PHPOffice/PhpSpreadsheet/issues/4261) [PR #4263](https://github.com/PHPOffice/PhpSpreadsheet/pull/4263) [PR #4264](https://github.com/PHPOffice/PhpSpreadsheet/pull/4264) [PR #4266](https://github.com/PHPOffice/PhpSpreadsheet/pull/4266) + +## 2024-12-08 - 3.6.0 + +### Added + +- Nothing yet. + +### Changed + +- Nothing yet. + +### Moved + +- Nothing yet. + +### Deprecated + +- Nothing yet. + +### Fixed + +- Html Reader/Writer Better Handling of Booleans. [PR #4257](https://github.com/PHPOffice/PhpSpreadsheet/pull/4257) +- Fill Patterns/Colors When Xml Attributes are Missing. [Issue #4248](https://github.com/PHPOffice/PhpSpreadsheet/issues/4248) [PR #4250](https://github.com/PHPOffice/PhpSpreadsheet/pull/4250) +- Remove Unneccesary files from Composer Package. [PR #4262](https://github.com/PHPOffice/PhpSpreadsheet/pull/4262) +- Swapped row and column indexes in ReferenceHelper. [Issue #4246](https://github.com/PHPOffice/PhpSpreadsheet/issues/4246) [PR #4247](https://github.com/PHPOffice/PhpSpreadsheet/pull/4247) +- Fix minor break handling drawings. [Issue #4241](https://github.com/PHPOffice/PhpSpreadsheet/issues/4241) [PR #4244](https://github.com/PHPOffice/PhpSpreadsheet/pull/4244) +- Ignore cell formatting when the format is a single @. [Issue #4242](https://github.com/PHPOffice/PhpSpreadsheet/issues/4242) [PR #4243](https://github.com/PHPOffice/PhpSpreadsheet/pull/4243) +- Upgrade Dompdf to Php-8.4 compatible version [PR #4267](https://github.com/PHPOffice/PhpSpreadsheet/pull/4267) + +## 2024-11-22 - 3.5.0 + +### Added + +- Nothing yet. + +### Changed + +- Settings::libXmlLoaderOptions is ignored. [PR #4233](https://github.com/PHPOffice/PhpSpreadsheet/pull/4233) + +### Moved + +- Nothing yet. + +### Deprecated + +- Settings::setLibXmlLoaderOptions() and Settings::getLibXmlLoaderOptions() are no longer needed - no replacement. +- Worksheet::getHashCode is no longer needed. + +### Fixed + +- Add support for `` tag when converting HTML to RichText. [Issue #4223](https://github.com/PHPOffice/PhpSpreadsheet/issues/4223) [PR #4224](https://github.com/PHPOffice/PhpSpreadsheet/pull/4224) +- Change hash code for worksheet. [Issue #4192](https://github.com/PHPOffice/PhpSpreadsheet/issues/4192) [PR #4207](https://github.com/PHPOffice/PhpSpreadsheet/pull/4207) + +## 2024-11-10 - 3.4.0 + +### Security Fix + +- Several security patches. + +### Added + +- Add Dynamic valueBinder Property to Spreadsheet and Readers. [Issue #1395](https://github.com/PHPOffice/PhpSpreadsheet/issues/1395) [PR #4185](https://github.com/PHPOffice/PhpSpreadsheet/pull/4185) +- Allow Omitting Chart Border. [Issue #562](https://github.com/PHPOffice/PhpSpreadsheet/issues/562) [PR #4188](https://github.com/PHPOffice/PhpSpreadsheet/pull/4188) +- Method to Test Whether Csv Will Be Affected by Php9. [PR #4189](https://github.com/PHPOffice/PhpSpreadsheet/pull/4189) + +### Changed + +- Refactor Xls Reader. [PR #4118](https://github.com/PHPOffice/PhpSpreadsheet/pull/4118) + +### Deprecated + +- IReader::SKIP_EMPTY_CELLS - use its alias IGNORE_EMPTY_CELLS instead. +- Worksheet::getProtectedCells was deprecated in release 2, but was not properly documented, and not removed in release 3. Use getProtectedCellRanges instead. +- Writer/Html::isMpdf property was deprecated in release 2, but was not properly documented, and not removed in release 3. Use instanceof Mpdf instead. + +### Moved + +- Nothing yet. + +### Fixed + +- Xls Writer Condtional Rules Applied to Whole Rows or Columns. [Issue #3185](https://github.com/PHPOffice/PhpSpreadsheet/issues/3185) [PR #4152](https://github.com/PHPOffice/PhpSpreadsheet/pull/4152) +- Xlsx Writer Duplicate ContentTypes Entry for Background Image. [Issue #4179](https://github.com/PHPOffice/PhpSpreadsheet/issues/4179) [PR #4180](https://github.com/PHPOffice/PhpSpreadsheet/pull/4180) +- Check strictNullComparison outside of loops. [PR #3347](https://github.com/PHPOffice/PhpSpreadsheet/pull/3347) +- SUMIFS Does Not Require xlfn. [Issue #4182](https://github.com/PHPOffice/PhpSpreadsheet/issues/4182) [PR #4186](https://github.com/PHPOffice/PhpSpreadsheet/pull/4186) +- Image Transparency/Opacity with Html Reader Changes. [Discussion #4117](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4117) [PR #4142](https://github.com/PHPOffice/PhpSpreadsheet/pull/4142) +- Option to Write Hyperlink Rather Than Label to Csv. [Issue #1412](https://github.com/PHPOffice/PhpSpreadsheet/issues/1412) [PR #4151](https://github.com/PHPOffice/PhpSpreadsheet/pull/4151) +- Invalid Html Due to Cached Filesize. [Issue #1107](https://github.com/PHPOffice/PhpSpreadsheet/issues/1107) [PR #4184](https://github.com/PHPOffice/PhpSpreadsheet/pull/4184) +- Excel 2003 Allows Html Entities. [Issue #2157](https://github.com/PHPOffice/PhpSpreadsheet/issues/2157) [PR #4187](https://github.com/PHPOffice/PhpSpreadsheet/pull/4187) +- Changes to ROUNDDOWN/ROUNDUP/TRUNC. [Issue #4213](https://github.com/PHPOffice/PhpSpreadsheet/issues/4213) [PR #4214](https://github.com/PHPOffice/PhpSpreadsheet/pull/4214) +- Writer Xlsx ignoredErrors Before Drawings. [Issue #4200](https://github.com/PHPOffice/PhpSpreadsheet/issues/4200) [Issue #4145](https://github.com/PHPOffice/PhpSpreadsheet/issues/4145) [PR #4212](https://github.com/PHPOffice/PhpSpreadsheet/pull/4212) +- Allow ANCHORARRAY as Data Validation list. [Issue #4197](https://github.com/PHPOffice/PhpSpreadsheet/issues/4197) [PR #4203](https://github.com/PHPOffice/PhpSpreadsheet/pull/4203) + +## 2024-09-29 - 3.3.0 (no 3.0.\*, 3.1.\*, 3.2.\*) + +### Dynamic Arrays + +- Support for Excel dynamic arrays is added. It is an opt-in feature, so our hope is that there will be no BC breaks, but it is a very large change. Full support is added for Xlsx. It is emulated as Ctrl-Shift-Enter arrays for Ods read and write and Excel2003 and Gnumeric read. Html/Pdf and Csv writers will populate cells on output if they are the result of array formulas. No support is added for Xls or Slk. + +### Added + +- Excel Dynamic Arrays. [Issue #3901](https://github.com/PHPOffice/PhpSpreadsheet/issues/3901) [Issue #3659](https://github.com/PHPOffice/PhpSpreadsheet/issues/3659) [Issue #1834](https://github.com/PHPOffice/PhpSpreadsheet/issues/1834) [PR #3962](https://github.com/PHPOffice/PhpSpreadsheet/pull/3962) +- String Value Binder Allow Setting "Ignore Number Stored as Text". [PR #4141](https://github.com/PHPOffice/PhpSpreadsheet/pull/4141) + +### Changed + +- Xlsx Reader default datatype when none is specified in Xml is changed from string to numeric, which is how Excel treats it. There is expected to be little impact because DefaultValueBinder and AdvancedValueBinder correct mis-identification as string, and StringValueBinder usually expects string. [PR #4139](https://github.com/PHPOffice/PhpSpreadsheet/pull/4139) +- Currency and Accounting Wizards are changed to act like Excel, and a new CurrencyBase Wizard is added for for non-Excel formats. [Issue #4125](https://github.com/PHPOffice/PhpSpreadsheet/issues/4125) [Issue #4124](https://github.com/PHPOffice/PhpSpreadsheet/issues/4124) [PR #4127](https://github.com/PHPOffice/PhpSpreadsheet/pull/4127) +- Images will not be added to spreadsheet if they cannot be validated as images. + +### Deprecated + +- Nothing yet. + +### Removed + +- The following items were deprecated in release 2 and are now removed. +- Writer\Xls\Style\ColorMap (no longer needed). +- Reader\Xml::trySimpleXMLLoadString (should not have been public, no public replacement). +- Calculation\Calculation::_translateFormulaToLocale (use method name translateFormulaToLocale without leading underscore). +- Calculation\Calculation::_translateFormulaToEnglish (use method name translateFormulaToEnglish without leading underscore). + +### Moved + +- Nothing yet. + +### Fixed + +- Several security patches. +- Xls Reader Some Ranges Not Handled Properly. [Issue #1570](https://github.com/PHPOffice/PhpSpreadsheet/issues/1570) [PR #4140](https://github.com/PHPOffice/PhpSpreadsheet/pull/4140) +- Better Handling of legacyDrawing Xml. [Issue #4105](https://github.com/PHPOffice/PhpSpreadsheet/issues/4105) [PR #4122](https://github.com/PHPOffice/PhpSpreadsheet/pull/4122) +- Improve Xlsx Reader Speed. [Issue #3917](https://github.com/PHPOffice/PhpSpreadsheet/issues/3917) [PR #4153](https://github.com/PHPOffice/PhpSpreadsheet/pull/4153) + +## 2024-08-07 - 2.2.2 + +### Added + +- Nothing yet. + +### Changed + +- Nothing yet. + +### Deprecated + +- Nothing yet. + +### Moved + +- Nothing yet. + +### Fixed + +- Html Reader Preserve Unicode Whitespace. [Issue #1284](https://github.com/PHPOffice/PhpSpreadsheet/issues/1284) [PR #4106](https://github.com/PHPOffice/PhpSpreadsheet/pull/4106) +- RATE Function Floating Point Number of Periods. [PR #4107](https://github.com/PHPOffice/PhpSpreadsheet/pull/4107) +- Parameter Name Change Xlsx Writer Workbook. [Issue #4108](https://github.com/PHPOffice/PhpSpreadsheet/issues/4108) [PR #4111](https://github.com/PHPOffice/PhpSpreadsheet/pull/4111) +- New Algorithm for TRUNC, ROUNDUP, ROUNDDOWN. [Issue #4113](https://github.com/PHPOffice/PhpSpreadsheet/issues/4113) [PR #4115](https://github.com/PHPOffice/PhpSpreadsheet/pull/4115) +- Worksheet applyStylesFromArray Retain Active Cell (Excel 16 was having a problem with some files). [Issue #4128](https://github.com/PHPOffice/PhpSpreadsheet/issues/4128) [PR #4132](https://github.com/PHPOffice/PhpSpreadsheet/pull/4132) + +## 2024-07-29 - 2.2.1 + +### Security Fix + +- Prevent XXE when loading files [PR #4119](https://github.com/PHPOffice/PhpSpreadsheet/pull/4119) + +### Fixed + +- Add Sheet may leave Active Sheet uninitialized. [Issue #4112](https://github.com/PHPOffice/PhpSpreadsheet/issues/4112) [PR #4114](https://github.com/PHPOffice/PhpSpreadsheet/pull/4114) +- Reference to Defined Name Specifying Worksheet. [Issue #206](https://github.com/PHPOffice/PhpSpreadsheet/issues/296) [PR #4096](https://github.com/PHPOffice/PhpSpreadsheet/pull/4096) +- Xls Reader Print/Show Gridlines. [Issue #912](https://github.com/PHPOffice/PhpSpreadsheet/issues/912) [PR #4098](https://github.com/PHPOffice/PhpSpreadsheet/pull/4098) +- ODS Reader Allow Omission of Page Settings Tags. [Issue #4099](https://github.com/PHPOffice/PhpSpreadsheet/issues/4099) [PR #4101](https://github.com/PHPOffice/PhpSpreadsheet/pull/4101) + +## 2024-07-24 - 2.2.0 + +### Added + +- Xlsx Reader Optionally Ignore Rows With No Cells. [Issue #3982](https://github.com/PHPOffice/PhpSpreadsheet/issues/3982) [PR #4035](https://github.com/PHPOffice/PhpSpreadsheet/pull/4035) +- Means to change style without affecting current cell/sheet. [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073) +- Option for CSV output file to have varying numbers of columns for each row. [Issue #1415](https://github.com/PHPOffice/PhpSpreadsheet/issues/1415) [PR #4076](https://github.com/PHPOffice/PhpSpreadsheet/pull/4076) + +### Changed + +- On read, Xlsx Reader had been breaking up union ranges into separate individual ranges. It will now try to preserve range as it was read in. [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042) +- Xlsx/Xls spreadsheet calculation and formatting of dates will use base date of spreadsheet even when spreadsheets with different base dates are simultaneously open. [Issue #1036](https://github.com/PHPOffice/PhpSpreadsheet/issues/1036) [Issue #1635](https://github.com/PHPOffice/PhpSpreadsheet/issues/1635) [PR #4071](https://github.com/PHPOffice/PhpSpreadsheet/pull/4071) + +### Deprecated + +- Writer\Xls\Style\ColorMap is no longer needed. + +### Moved + +- Nothing + +### Fixed + +- Incorrect Reader CSV with BOM. [Issue #4028](https://github.com/PHPOffice/PhpSpreadsheet/issues/4028) [PR #4029](https://github.com/PHPOffice/PhpSpreadsheet/pull/4029) +- POWER Null/Bool Args. [PR #4031](https://github.com/PHPOffice/PhpSpreadsheet/pull/4031) +- Do Not Output Alignment and Protection for Conditional Format. [Issue #4025](https://github.com/PHPOffice/PhpSpreadsheet/issues/4025) [PR #4027](https://github.com/PHPOffice/PhpSpreadsheet/pull/4027) +- Conditional Color Scale Improvements. [Issue #4049](https://github.com/PHPOffice/PhpSpreadsheet/issues/4049) [PR #4050](https://github.com/PHPOffice/PhpSpreadsheet/pull/4050) +- Mpdf and Tcpdf Borders on Merged Cells. [Issue #3557](https://github.com/PHPOffice/PhpSpreadsheet/issues/3557) [PR #4047](https://github.com/PHPOffice/PhpSpreadsheet/pull/4047) +- Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033) +- Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042) +- Ods comments with newlines. [Issue #4081](https://github.com/PHPOffice/PhpSpreadsheet/issues/4081) [PR #4086](https://github.com/PHPOffice/PhpSpreadsheet/pull/4086) +- Propagate errors in Text functions. [Issue #2581](https://github.com/PHPOffice/PhpSpreadsheet/issues/2581) [PR #4080](https://github.com/PHPOffice/PhpSpreadsheet/pull/4080) +- Csv Reader allow use of html mimetype. [Issue #4036](https://github.com/PHPOffice/PhpSpreadsheet/issues/4036) [PR #4040](https://github.com/PHPOffice/PhpSpreadsheet/pull/4040) +- Problem rendering line chart with missing plot label. [PR #4074](https://github.com/PHPOffice/PhpSpreadsheet/pull/4074) +- More RTL in Xlsx/Html Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4065](https://github.com/PHPOffice/PhpSpreadsheet/pull/4065) +- Empty String in sharedStrings. [Issue #4063](https://github.com/PHPOffice/PhpSpreadsheet/issues/4063) [PR #4064](https://github.com/PHPOffice/PhpSpreadsheet/pull/4064) +- Xlsx Writer RichText and TYPE_STRING. [Issue #476](https://github.com/PHPOffice/PhpSpreadsheet/issues/476) [PR #4094](https://github.com/PHPOffice/PhpSpreadsheet/pull/4094) +- Ods boolean data. [Issue #460](https://github.com/PHPOffice/PhpSpreadsheet/issues/460) [PR #4093](https://github.com/PHPOffice/PhpSpreadsheet/pull/4093) +- Html Writer Minor Fixes. [PR #4089](https://github.com/PHPOffice/PhpSpreadsheet/pull/4089) +- Changes to INDEX function. [Issue #64](https://github.com/PHPOffice/PhpSpreadsheet/issues/64) [PR #4088](https://github.com/PHPOffice/PhpSpreadsheet/pull/4088) +- Ods Reader and Whitespace Text Nodes. [Issue #804](https://github.com/PHPOffice/PhpSpreadsheet/issues/804) [PR #4087](https://github.com/PHPOffice/PhpSpreadsheet/pull/4087) +- Ods Xml Reader and Whitespace Text Nodes. [Issue #804](https://github.com/PHPOffice/PhpSpreadsheet/issues/804) [PR #4087](https://github.com/PHPOffice/PhpSpreadsheet/pull/4087) +- Treat invalid formulas as strings. [Issue #1310](https://github.com/PHPOffice/PhpSpreadsheet/issues/1310) [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073) + +## 2024-05-11 - 2.1.0 + +### MINOR BREAKING CHANGE + +- Writing of cell comments to Html will now sanitize all Html tags within the comment, so the tags will be rendered as plaintext and have no other effects when rendered. Styling can be achieved by using the Font property of of the TextRuns which make up the comment, as is already the cases for Xlsx. [PR #3957](https://github.com/PHPOffice/PhpSpreadsheet/pull/3957) + +### Added + +- Default Style Alignment Property (workaround for bug in non-Excel spreadsheet apps) [Issue #3918](https://github.com/PHPOffice/PhpSpreadsheet/issues/3918) [PR #3924](https://github.com/PHPOffice/PhpSpreadsheet/pull/3924) +- Additional Support for Date/Time Styles [PR #3939](https://github.com/PHPOffice/PhpSpreadsheet/pull/3939) + +### Changed + +- Nothing + +### Deprecated + +- Reader/Xml trySimpleXMLLoadString should not have had public visibility, and will be removed. + +### Removed + +- Nothing + +### Fixed + +- IF Empty Arguments. [Issue #3875](https://github.com/PHPOffice/PhpSpreadsheet/issues/3875) [Issue #2146](https://github.com/PHPOffice/PhpSpreadsheet/issues/2146) [PR #3879](https://github.com/PHPOffice/PhpSpreadsheet/pull/3879) +- Changes to floating point in Php8.4. [Issue #3896](https://github.com/PHPOffice/PhpSpreadsheet/issues/3896) [PR #3897](https://github.com/PHPOffice/PhpSpreadsheet/pull/3897) +- Handling User-supplied Decimal and Thousands Separators. [Issue #3900](https://github.com/PHPOffice/PhpSpreadsheet/issues/3900) [PR #3903](https://github.com/PHPOffice/PhpSpreadsheet/pull/3903) +- Improve Performance of CSV Writer. [Issue #3904](https://github.com/PHPOffice/PhpSpreadsheet/issues/3904) [PR #3906](https://github.com/PHPOffice/PhpSpreadsheet/pull/3906) +- Fix issue with prepending zero in percentage [Issue #3920](https://github.com/PHPOffice/PhpSpreadsheet/issues/3920) [PR #3921](https://github.com/PHPOffice/PhpSpreadsheet/pull/3921) +- Incorrect SUMPRODUCT Calculation [Issue #3909](https://github.com/PHPOffice/PhpSpreadsheet/issues/3909) [PR #3916](https://github.com/PHPOffice/PhpSpreadsheet/pull/3916) +- Formula Misidentifying Text as Cell After Insertion/Deletion [Issue #3907](https://github.com/PHPOffice/PhpSpreadsheet/issues/3907) [PR #3915](https://github.com/PHPOffice/PhpSpreadsheet/pull/3915) +- Unexpected Absolute Address in Xlsx Rels [Issue #3730](https://github.com/PHPOffice/PhpSpreadsheet/issues/3730) [PR #3923](https://github.com/PHPOffice/PhpSpreadsheet/pull/3923) +- Unallocated Cells Affected by Column/Row Insert/Delete [Issue #3933](https://github.com/PHPOffice/PhpSpreadsheet/issues/3933) [PR #3940](https://github.com/PHPOffice/PhpSpreadsheet/pull/3940) +- Invalid Builtin Defined Name in Xls Reader [Issue #3935](https://github.com/PHPOffice/PhpSpreadsheet/issues/3935) [PR #3942](https://github.com/PHPOffice/PhpSpreadsheet/pull/3942) +- Hidden Rows and Columns Tcpdf/Mpdf [PR #3945](https://github.com/PHPOffice/PhpSpreadsheet/pull/3945) +- RTL Text Alignment in Xlsx Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4006](https://github.com/PHPOffice/PhpSpreadsheet/pull/4006) +- Protect Sheet But Allow Sort [Issue #3951](https://github.com/PHPOffice/PhpSpreadsheet/issues/3951) [PR #3956](https://github.com/PHPOffice/PhpSpreadsheet/pull/3956) +- Default Value for Conditional::$text [PR #3946](https://github.com/PHPOffice/PhpSpreadsheet/pull/3946) +- Table Filter Buttons [Issue #3988](https://github.com/PHPOffice/PhpSpreadsheet/issues/3988) [PR #3992](https://github.com/PHPOffice/PhpSpreadsheet/pull/3992) +- Improvements to Xml Reader [Issue #3999](https://github.com/PHPOffice/PhpSpreadsheet/issues/3999) [Issue #4000](https://github.com/PHPOffice/PhpSpreadsheet/issues/4000) [Issue #4001](https://github.com/PHPOffice/PhpSpreadsheet/issues/4001) [Issue #4002](https://github.com/PHPOffice/PhpSpreadsheet/issues/4002) [PR #4003](https://github.com/PHPOffice/PhpSpreadsheet/pull/4003) [PR #4007](https://github.com/PHPOffice/PhpSpreadsheet/pull/4007) +- Html Reader non-UTF8 [Issue #3995](https://github.com/PHPOffice/PhpSpreadsheet/issues/3995) [Issue #866](https://github.com/PHPOffice/PhpSpreadsheet/issues/866) [Issue #1681](https://github.com/PHPOffice/PhpSpreadsheet/issues/1681) [PR #4019](https://github.com/PHPOffice/PhpSpreadsheet/pull/4019) + +## 2.0.0 - 2024-01-04 + +### BREAKING CHANGE + +- Typing was strengthened by leveraging native typing. This should not change any behavior. However, if you implement + any interfaces or inherit from any classes, you will need to adapt your typing accordingly. If you use static analysis + tools such as PHPStan or Psalm, new errors might be found. If you find actual bugs because of the new typing, please + open a PR that fixes it with a **detailed** explanation of the reason. We'll try to merge and release typing-related + fixes quickly in the coming days. [PR #3718](https://github.com/PHPOffice/PhpSpreadsheet/pull/3718) +- All deprecated things have been removed, for details, see [816b91d0b4](https://github.com/PHPOffice/PhpSpreadsheet/commit/816b91d0b4a0c7285a9e3fc88c58f7730d922044) + +### Added + +- Split screens (Xlsx and Xml only, not 100% complete). [Issue #3601](https://github.com/PHPOffice/PhpSpreadsheet/issues/3601) [PR #3622](https://github.com/PHPOffice/PhpSpreadsheet/pull/3622) +- Permit Meta Viewport in Html. [Issue #3565](https://github.com/PHPOffice/PhpSpreadsheet/issues/3565) [PR #3623](https://github.com/PHPOffice/PhpSpreadsheet/pull/3623) +- Hyperlink support for Ods. [Issue #3660](https://github.com/PHPOffice/PhpSpreadsheet/issues/3660) [PR #3669](https://github.com/PHPOffice/PhpSpreadsheet/pull/3669) +- ListWorksheetInfo/Names for Html/Csv/Slk. [Issue #3706](https://github.com/PHPOffice/PhpSpreadsheet/issues/3706) [PR #3709](https://github.com/PHPOffice/PhpSpreadsheet/pull/3709) +- Methods to determine if cell is actually locked, or hidden on formula bar. [PR #3722](https://github.com/PHPOffice/PhpSpreadsheet/pull/3722) +- Add iterateOnlyExistingCells to Constructors. [Issue #3721](https://github.com/PHPOffice/PhpSpreadsheet/issues/3721) [PR #3727](https://github.com/PHPOffice/PhpSpreadsheet/pull/3727) +- Support for Conditional Formatting Color Scale. [PR #3738](https://github.com/PHPOffice/PhpSpreadsheet/pull/3738) +- Support Additional Tags in Helper/Html. [Issue #3751](https://github.com/PHPOffice/PhpSpreadsheet/issues/3751) [PR #3752](https://github.com/PHPOffice/PhpSpreadsheet/pull/3752) +- Writer ODS : Write Border Style for cells [Issue #3690](https://github.com/PHPOffice/PhpSpreadsheet/issues/3690) [PR #3693](https://github.com/PHPOffice/PhpSpreadsheet/pull/3693) +- Sheet Background Images [Issue #1649](https://github.com/PHPOffice/PhpSpreadsheet/issues/1649) [PR #3795](https://github.com/PHPOffice/PhpSpreadsheet/pull/3795) +- Check if Coordinate is Inside Range [PR #3779](https://github.com/PHPOffice/PhpSpreadsheet/pull/3779) +- Flipping Images [Issue #731](https://github.com/PHPOffice/PhpSpreadsheet/issues/731) [PR #3801](https://github.com/PHPOffice/PhpSpreadsheet/pull/3801) +- Chart Dynamic Title and Font Properties [Issue #3797](https://github.com/PHPOffice/PhpSpreadsheet/issues/3797) [PR #3800](https://github.com/PHPOffice/PhpSpreadsheet/pull/3800) +- Chart Axis Display Units and Logarithmic Scale. [Issue #3833](https://github.com/PHPOffice/PhpSpreadsheet/issues/3833) [PR #3836](https://github.com/PHPOffice/PhpSpreadsheet/pull/3836) +- Partial Support of Fill Handles. [Discussion #3847](https://github.com/PHPOffice/PhpSpreadsheet/discussions/3847) [PR #3855](https://github.com/PHPOffice/PhpSpreadsheet/pull/3855) + +### Changed + +- **Drop support for PHP 7.4**, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support [PR #3713](https://github.com/PHPOffice/PhpSpreadsheet/pull/3713) +- RLM Added to NumberFormatter Currency. This happens depending on release of ICU which Php is using (it does not yet happen with any official release). PhpSpreadsheet will continue to use the value returned by Php, but a method is added to keep the result unchanged from release to release. [Issue #3571](https://github.com/PHPOffice/PhpSpreadsheet/issues/3571) [PR #3640](https://github.com/PHPOffice/PhpSpreadsheet/pull/3640) +- `toFormattedString` will now always return a string. This was introduced with 1.28.0, but was not properly documented at the time. This can affect the results of `toArray`, `namedRangeToArray`, and `rangeToArray`. [PR #3304](https://github.com/PHPOffice/PhpSpreadsheet/pull/3304) +- Value of constants FORMAT_CURRENCY_EUR and FORMAT_CURRENCY_USD was changed in 1.28.0, but was not properly documented at the time. [Issue #3577](https://github.com/PHPOffice/PhpSpreadsheet/issues/3577) +- Html Writer will attempt to use Chart coordinates to determine image size. [Issue #3783](https://github.com/PHPOffice/PhpSpreadsheet/issues/3783) [PR #3787](https://github.com/PHPOffice/PhpSpreadsheet/pull/3787) + +### Deprecated + +- Functions `_translateFormulaToLocale` and `_translateFormulaEnglish` are replaced by versions without leading underscore. [PR #3828](https://github.com/PHPOffice/PhpSpreadsheet/pull/3828) + +### Removed + +- Nothing + +### Fixed + +- Take advantage of mitoteam/jpgraph Extended mode to enable rendering of more graphs. [PR #3603](https://github.com/PHPOffice/PhpSpreadsheet/pull/3603) +- Column widths, especially for ODS. [Issue #3609](https://github.com/PHPOffice/PhpSpreadsheet/issues/3609) [PR #3610](https://github.com/PHPOffice/PhpSpreadsheet/pull/3610) +- Avoid NULL in String Function call (partial solution). [Issue #3613](https://github.com/PHPOffice/PhpSpreadsheet/issues/3613) [PR #3617](https://github.com/PHPOffice/PhpSpreadsheet/pull/3617) +- Preserve transparency in Memory Drawing. [Issue #3624](https://github.com/PHPOffice/PhpSpreadsheet/issues/3624) [PR #3627](https://github.com/PHPOffice/PhpSpreadsheet/pull/3627) +- Customizable padding for Exact Column Width. [Issue #3626](https://github.com/PHPOffice/PhpSpreadsheet/issues/3626) [PR #3628](https://github.com/PHPOffice/PhpSpreadsheet/pull/3628) +- Ensure ROW function returns int (problem exposed in unreleased Php). [PR #3641](https://github.com/PHPOffice/PhpSpreadsheet/pull/3641) +- Minor changes to Mpdf and Html Writers. [PR #3645](https://github.com/PHPOffice/PhpSpreadsheet/pull/3645) +- Xlsx Reader Namespacing for Tables, Autofilters. [Issue #3665](https://github.com/PHPOffice/PhpSpreadsheet/issues/3665) [PR #3668](https://github.com/PHPOffice/PhpSpreadsheet/pull/3668) +- Read Code Page for Xls ListWorksheetInfo/Names BIFF5. [Issue #3671](https://github.com/PHPOffice/PhpSpreadsheet/issues/3671) [PR #3672](https://github.com/PHPOffice/PhpSpreadsheet/pull/3672) +- Read Data from Table on Different Sheet. [Issue #3635](https://github.com/PHPOffice/PhpSpreadsheet/issues/3635) [PR #3659](https://github.com/PHPOffice/PhpSpreadsheet/pull/3659) +- Html Writer Styles Using Inline Css. [Issue #3678](https://github.com/PHPOffice/PhpSpreadsheet/issues/3678) [PR #3680](https://github.com/PHPOffice/PhpSpreadsheet/pull/3680) +- Xlsx Read Ignoring Some Comments. [Issue #3654](https://github.com/PHPOffice/PhpSpreadsheet/issues/3654) [PR #3655](https://github.com/PHPOffice/PhpSpreadsheet/pull/3655) +- Fractional Seconds in Date/Time Values. [PR #3677](https://github.com/PHPOffice/PhpSpreadsheet/pull/3677) +- SetCalculatedValue Avoid Casting String to Numeric. [Issue #3658](https://github.com/PHPOffice/PhpSpreadsheet/issues/3658) [PR #3685](https://github.com/PHPOffice/PhpSpreadsheet/pull/3685) +- Several Problems in a Very Complicated Spreadsheet. [Issue #3679](https://github.com/PHPOffice/PhpSpreadsheet/issues/3679) [PR #3681](https://github.com/PHPOffice/PhpSpreadsheet/pull/3681) +- Inconsistent String Handling for Sum Functions. [Issue #3652](https://github.com/PHPOffice/PhpSpreadsheet/issues/3652) [PR #3653](https://github.com/PHPOffice/PhpSpreadsheet/pull/3653) +- Recomputation of Relative Addresses in Defined Names. [Issue #3661](https://github.com/PHPOffice/PhpSpreadsheet/issues/3661) [PR #3673](https://github.com/PHPOffice/PhpSpreadsheet/pull/3673) +- Writer Xls Characters Outside BMP (emojis). [Issue #642](https://github.com/PHPOffice/PhpSpreadsheet/issues/642) [PR #3696](https://github.com/PHPOffice/PhpSpreadsheet/pull/3696) +- Xlsx Reader Improve Handling of Row and Column Styles. [Issue #3533](https://github.com/PHPOffice/PhpSpreadsheet/issues/3533) [Issue #3534](https://github.com/PHPOffice/PhpSpreadsheet/issues/3534) [PR #3688](https://github.com/PHPOffice/PhpSpreadsheet/pull/3688) +- Avoid Allocating RowDimension Unneccesarily. [PR #3686](https://github.com/PHPOffice/PhpSpreadsheet/pull/3686) +- Use Column Style when Row Dimension Exists Without Style. [Issue #3534](https://github.com/PHPOffice/PhpSpreadsheet/issues/3534) [PR #3688](https://github.com/PHPOffice/PhpSpreadsheet/pull/3688) +- Inconsistency Between Cell Data and Explicitly Declared Type. [Issue #3711](https://github.com/PHPOffice/PhpSpreadsheet/issues/3711) [PR #3715](https://github.com/PHPOffice/PhpSpreadsheet/pull/3715) +- Unexpected Namespacing in rels File. [Issue #3720](https://github.com/PHPOffice/PhpSpreadsheet/issues/3720) [PR #3722](https://github.com/PHPOffice/PhpSpreadsheet/pull/3722) +- Break Some Circular References. [PR #3716](https://github.com/PHPOffice/PhpSpreadsheet/pull/3716) [PR #3707](https://github.com/PHPOffice/PhpSpreadsheet/pull/3707) +- Missing Font Index in Some Xls. [PR #3734](https://github.com/PHPOffice/PhpSpreadsheet/pull/3734) +- Load Tables even with READ_DATA_ONLY. [PR #3726](https://github.com/PHPOffice/PhpSpreadsheet/pull/3726) +- Theme File Missing but Referenced in Spreadsheet. [Issue #3770](https://github.com/PHPOffice/PhpSpreadsheet/issues/3770) [PR #3772](https://github.com/PHPOffice/PhpSpreadsheet/pull/3772) +- Slk Shared Formulas. [Issue #2267](https://github.com/PHPOffice/PhpSpreadsheet/issues/2267) [PR #3776](https://github.com/PHPOffice/PhpSpreadsheet/pull/3776) +- Html omitting some charts. [Issue #3767](https://github.com/PHPOffice/PhpSpreadsheet/issues/3767) [PR #3771](https://github.com/PHPOffice/PhpSpreadsheet/pull/3771) +- Case Insensitive Comparison for Sheet Names [PR #3791](https://github.com/PHPOffice/PhpSpreadsheet/pull/3791) +- Performance improvement for Xlsx Reader. [Issue #3683](https://github.com/PHPOffice/PhpSpreadsheet/issues/3683) [PR #3810](https://github.com/PHPOffice/PhpSpreadsheet/pull/3810) +- Prevent loop in Shared/File. [Issue #3807](https://github.com/PHPOffice/PhpSpreadsheet/issues/3807) [PR #3809](https://github.com/PHPOffice/PhpSpreadsheet/pull/3809) +- Consistent handling of decimal/thousands separators between StringHelper and Php setlocale. [Issue #3811](https://github.com/PHPOffice/PhpSpreadsheet/issues/3811) [PR #3815](https://github.com/PHPOffice/PhpSpreadsheet/pull/3815) +- Clone worksheet with tables or charts. [Issue #3820](https://github.com/PHPOffice/PhpSpreadsheet/issues/3820) [PR #3821](https://github.com/PHPOffice/PhpSpreadsheet/pull/3821) +- COUNTIFS Does Not Require xlfn. [Issue #3819](https://github.com/PHPOffice/PhpSpreadsheet/issues/3819) [PR #3827](https://github.com/PHPOffice/PhpSpreadsheet/pull/3827) +- Strip `xlfn.` and `xlws.` from Formula Translations. [Issue #3819](https://github.com/PHPOffice/PhpSpreadsheet/issues/3819) [PR #3828](https://github.com/PHPOffice/PhpSpreadsheet/pull/3828) +- Recurse directories searching for font file. [Issue #2809](https://github.com/PHPOffice/PhpSpreadsheet/issues/2809) [PR #3830](https://github.com/PHPOffice/PhpSpreadsheet/pull/3830) +- Reduce memory consumption of Worksheet::rangeToArray() when many empty rows are read. [Issue #3814](https://github.com/PHPOffice/PhpSpreadsheet/issues/3814) [PR #3834](https://github.com/PHPOffice/PhpSpreadsheet/pull/3834) +- Reduce time used by Worksheet::rangeToArray() when many empty rows are read. [PR #3839](https://github.com/PHPOffice/PhpSpreadsheet/pull/3839) +- Html Reader Tolerate Invalid Sheet Title. [PR #3845](https://github.com/PHPOffice/PhpSpreadsheet/pull/3845) +- Do not include unparsed drawings when new drawing added. [Issue #3843](https://github.com/PHPOffice/PhpSpreadsheet/issues/3843) [PR #3846](https://github.com/PHPOffice/PhpSpreadsheet/pull/3846) +- Do not include unparsed drawings when new drawing added. [Issue #3861](https://github.com/PHPOffice/PhpSpreadsheet/issues/3861) [PR #3862](https://github.com/PHPOffice/PhpSpreadsheet/pull/3862) +- Excel omits `between` operator for data validation. [Issue #3863](https://github.com/PHPOffice/PhpSpreadsheet/issues/3863) [PR #3865](https://github.com/PHPOffice/PhpSpreadsheet/pull/3865) +- Use less space when inserting rows and columns. [Issue #3687](https://github.com/PHPOffice/PhpSpreadsheet/issues/3687) [PR #3856](https://github.com/PHPOffice/PhpSpreadsheet/pull/3856) +- Excel inconsistent handling of MIN/MAX/MINA/MAXA. [Issue #3866](https://github.com/PHPOffice/PhpSpreadsheet/issues/3866) [PR #3868](https://github.com/PHPOffice/PhpSpreadsheet/pull/3868) + +## 1.29.0 - 2023-06-15 + +### Added + +- Wizards for defining Number Format masks for Dates and Times, including Durations/Intervals. [PR #3458](https://github.com/PHPOffice/PhpSpreadsheet/pull/3458) +- Specify data type in html tags. [Issue #3444](https://github.com/PHPOffice/PhpSpreadsheet/issues/3444) [PR #3445](https://github.com/PHPOffice/PhpSpreadsheet/pull/3445) +- Provide option to ignore hidden rows/columns in `toArray()` methods. [PR #3494](https://github.com/PHPOffice/PhpSpreadsheet/pull/3494) +- Font/Effects/Theme support for Chart Data Labels and Axis. [PR #3476](https://github.com/PHPOffice/PhpSpreadsheet/pull/3476) +- Font Themes support. [PR #3486](https://github.com/PHPOffice/PhpSpreadsheet/pull/3486) +- Ability to Ignore Cell Errors in Excel. [Issue #1141](https://github.com/PHPOffice/PhpSpreadsheet/issues/1141) [PR #3508](https://github.com/PHPOffice/PhpSpreadsheet/pull/3508) +- Unzipped Gnumeric file [PR #3591](https://github.com/PHPOffice/PhpSpreadsheet/pull/3591) + +### Changed + +- Xlsx Color schemes read in will be written out (previously Excel 2007-2010 Color scheme was always written); manipulation of those schemes before write, including restoring prior behavior, is provided [PR #3476](https://github.com/PHPOffice/PhpSpreadsheet/pull/3476) +- Memory and speed optimisations for Read Filters with Xlsx Files and Shared Formulae. [PR #3474](https://github.com/PHPOffice/PhpSpreadsheet/pull/3474) +- Allow `CellRange` and `CellAddress` objects for the `range` argument in the `rangeToArray()` method. [PR #3494](https://github.com/PHPOffice/PhpSpreadsheet/pull/3494) +- Stock charts will now read and reproduce `upDownBars` and subsidiary tags; these were previously ignored on read and hard-coded on write. [PR #3515](https://github.com/PHPOffice/PhpSpreadsheet/pull/3515) + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Updates Cell formula absolute ranges/references, and Defined Name absolute ranges/references when inserting/deleting rows/columns. [Issue #3368](https://github.com/PHPOffice/PhpSpreadsheet/issues/3368) [PR #3402](https://github.com/PHPOffice/PhpSpreadsheet/pull/3402) +- EOMONTH() and EDATE() Functions should round date value before evaluation. [Issue #3436](https://github.com/PHPOffice/PhpSpreadsheet/issues/3436) [PR #3437](https://github.com/PHPOffice/PhpSpreadsheet/pull/3437) +- NETWORKDAYS function erroneously being converted to NETWORK_xlfn.DAYS in Xlsx Writer. [Issue #3461](https://github.com/PHPOffice/PhpSpreadsheet/issues/3461) [PR #3463](https://github.com/PHPOffice/PhpSpreadsheet/pull/3463) +- Getting a style for a CellAddress instance fails if the worksheet is set in the CellAddress instance. [Issue #3439](https://github.com/PHPOffice/PhpSpreadsheet/issues/3439) [PR #3469](https://github.com/PHPOffice/PhpSpreadsheet/pull/3469) +- Shared Formulae outside the filter range when reading with a filter are not always being identified. [Issue #3473](https://github.com/PHPOffice/PhpSpreadsheet/issues/3473) [PR #3474](https://github.com/PHPOffice/PhpSpreadsheet/pull/3474) +- Xls Reader Conditional Styles. [PR #3400](https://github.com/PHPOffice/PhpSpreadsheet/pull/3400) +- Allow use of # and 0 digit placeholders in fraction masks. [PR #3401](https://github.com/PHPOffice/PhpSpreadsheet/pull/3401) +- Modify Date/Time check in the NumberFormatter for decimal/fractional times. [PR #3413](https://github.com/PHPOffice/PhpSpreadsheet/pull/3413) +- Misplaced Xml Writing Chart Label FillColor. [Issue #3397](https://github.com/PHPOffice/PhpSpreadsheet/issues/3397) [PR #3404](https://github.com/PHPOffice/PhpSpreadsheet/pull/3404) +- TEXT function ignores Time in DateTimeStamp. [Issue #3409](https://github.com/PHPOffice/PhpSpreadsheet/issues/3409) [PR #3411](https://github.com/PHPOffice/PhpSpreadsheet/pull/3411) +- Xlsx Column Autosize Approximate for CJK. [Issue #3405](https://github.com/PHPOffice/PhpSpreadsheet/issues/3405) [PR #3416](https://github.com/PHPOffice/PhpSpreadsheet/pull/3416) +- Correct Xlsx Parsing of quotePrefix="0". [Issue #3435](https://github.com/PHPOffice/PhpSpreadsheet/issues/3435) [PR #3438](https://github.com/PHPOffice/PhpSpreadsheet/pull/3438) +- More Display Options for Chart Axis and Legend. [Issue #3414](https://github.com/PHPOffice/PhpSpreadsheet/issues/3414) [PR #3434](https://github.com/PHPOffice/PhpSpreadsheet/pull/3434) +- Apply strict type checking to Complex suffix. [PR #3452](https://github.com/PHPOffice/PhpSpreadsheet/pull/3452) +- Incorrect Font Color Read Xlsx Rich Text Indexed Color Custom Palette. [Issue #3464](https://github.com/PHPOffice/PhpSpreadsheet/issues/3464) [PR #3465](https://github.com/PHPOffice/PhpSpreadsheet/pull/3465) +- Xlsx Writer Honor Alignment in Default Font. [Issue #3443](https://github.com/PHPOffice/PhpSpreadsheet/issues/3443) [PR #3459](https://github.com/PHPOffice/PhpSpreadsheet/pull/3459) +- Support Border for Charts. [PR #3462](https://github.com/PHPOffice/PhpSpreadsheet/pull/3462) +- Error in "this row" structured reference calculation (cached result from first row when using a range) [Issue #3504](https://github.com/PHPOffice/PhpSpreadsheet/issues/3504) [PR #3505](https://github.com/PHPOffice/PhpSpreadsheet/pull/3505) +- Allow colour palette index references in Number Format masks [Issue #3511](https://github.com/PHPOffice/PhpSpreadsheet/issues/3511) [PR #3512](https://github.com/PHPOffice/PhpSpreadsheet/pull/3512) +- Xlsx Reader formula with quotePrefix [Issue #3495](https://github.com/PHPOffice/PhpSpreadsheet/issues/3495) [PR #3497](https://github.com/PHPOffice/PhpSpreadsheet/pull/3497) +- Handle REF error as part of range [Issue #3453](https://github.com/PHPOffice/PhpSpreadsheet/issues/3453) [PR #3467](https://github.com/PHPOffice/PhpSpreadsheet/pull/3467) +- Handle Absolute Pathnames in Rels File [Issue #3553](https://github.com/PHPOffice/PhpSpreadsheet/issues/3553) [PR #3554](https://github.com/PHPOffice/PhpSpreadsheet/pull/3554) +- Return Page Breaks in Order [Issue #3552](https://github.com/PHPOffice/PhpSpreadsheet/issues/3552) [PR #3555](https://github.com/PHPOffice/PhpSpreadsheet/pull/3555) +- Add position attribute for MemoryDrawing in Html [Issue #3529](https://github.com/PHPOffice/PhpSpreadsheet/issues/3529 [PR #3535](https://github.com/PHPOffice/PhpSpreadsheet/pull/3535) +- Allow Index_number as Array for VLOOKUP/HLOOKUP [Issue #3561](https://github.com/PHPOffice/PhpSpreadsheet/issues/3561 [PR #3570](https://github.com/PHPOffice/PhpSpreadsheet/pull/3570) +- Add Unsupported Options in Xml Spreadsheet [Issue #3566](https://github.com/PHPOffice/PhpSpreadsheet/issues/3566 [Issue #3568](https://github.com/PHPOffice/PhpSpreadsheet/issues/3568 [Issue #3569](https://github.com/PHPOffice/PhpSpreadsheet/issues/3569 [PR #3567](https://github.com/PHPOffice/PhpSpreadsheet/pull/3567) +- Changes to NUMBERVALUE, VALUE, DATEVALUE, TIMEVALUE [Issue #3574](https://github.com/PHPOffice/PhpSpreadsheet/issues/3574 [PR #3575](https://github.com/PHPOffice/PhpSpreadsheet/pull/3575) +- Redo calculation of color tinting [Issue #3550](https://github.com/PHPOffice/PhpSpreadsheet/issues/3550) [PR #3580](https://github.com/PHPOffice/PhpSpreadsheet/pull/3580) +- Accommodate Slash with preg_quote [PR #3582](https://github.com/PHPOffice/PhpSpreadsheet/pull/3582) [PR #3583](https://github.com/PHPOffice/PhpSpreadsheet/pull/3583) [PR #3584](https://github.com/PHPOffice/PhpSpreadsheet/pull/3584) +- HyperlinkBase Property and Html Handling of Properties [Issue #3573](https://github.com/PHPOffice/PhpSpreadsheet/issues/3573) [PR #3589](https://github.com/PHPOffice/PhpSpreadsheet/pull/3589) +- Improvements for Data Validation [Issue #3592](https://github.com/PHPOffice/PhpSpreadsheet/issues/3592) [Issue #3594](https://github.com/PHPOffice/PhpSpreadsheet/issues/3594) [PR #3605](https://github.com/PHPOffice/PhpSpreadsheet/pull/3605) + +## 1.28.0 - 2023-02-25 + +### Added + +- Support for configuring a Chart Title's overlay [PR #3325](https://github.com/PHPOffice/PhpSpreadsheet/pull/3325) +- Wizards for defining Number Format masks for Numbers, Percentages, Scientific, Currency and Accounting [PR #3334](https://github.com/PHPOffice/PhpSpreadsheet/pull/3334) +- Support for fixed value divisor in fractional Number Format Masks [PR #3339](https://github.com/PHPOffice/PhpSpreadsheet/pull/3339) +- Allow More Fonts/Fontnames for Exact Width Calculation [PR #3326](https://github.com/PHPOffice/PhpSpreadsheet/pull/3326) [Issue #3190](https://github.com/PHPOffice/PhpSpreadsheet/issues/3190) +- Allow override of the Value Binder when setting a Cell value [PR #3361](https://github.com/PHPOffice/PhpSpreadsheet/pull/3361) + +### Changed + +- Improved handling for @ placeholder in Number Format Masks [PR #3344](https://github.com/PHPOffice/PhpSpreadsheet/pull/3344) +- Improved handling for ? placeholder in Number Format Masks [PR #3394](https://github.com/PHPOffice/PhpSpreadsheet/pull/3394) +- Improved support for locale settings and currency codes when matching formatted strings to numerics in the Calculation Engine [PR #3373](https://github.com/PHPOffice/PhpSpreadsheet/pull/3373) and [PR #3374](https://github.com/PHPOffice/PhpSpreadsheet/pull/3374) +- Improved support for locale settings and matching in the Advanced Value Binder [PR #3376](https://github.com/PHPOffice/PhpSpreadsheet/pull/3376) +- `toFormattedString` will now always return a string. This can affect the results of `toArray`, `namedRangeToArray`, and `rangeToArray`. [PR #3304](https://github.com/PHPOffice/PhpSpreadsheet/pull/3304) +- Value of constants FORMAT_CURRENCY_EUR and FORMAT_CURRENCY_USD is changed. [Issue #3577](https://github.com/PHPOffice/PhpSpreadsheet/issues/3577) [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377) + +### Deprecated + +- Rationalisation of Pre-defined Currency Format Masks [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377) + +### Removed + +- Nothing + +### Fixed + +- Calculation Engine doesn't evaluate Defined Name when default cell A1 is quote-prefixed [Issue #3335](https://github.com/PHPOffice/PhpSpreadsheet/issues/3335) [PR #3336](https://github.com/PHPOffice/PhpSpreadsheet/pull/3336) +- XLSX Writer - Array Formulas do not include function prefix [Issue #3337](https://github.com/PHPOffice/PhpSpreadsheet/issues/3337) [PR #3338](https://github.com/PHPOffice/PhpSpreadsheet/pull/3338) +- Permit Max Column for Row Breaks [Issue #3143](https://github.com/PHPOffice/PhpSpreadsheet/issues/3143) [PR #3345](https://github.com/PHPOffice/PhpSpreadsheet/pull/3345) +- AutoSize Columns should allow for dropdown icon when AutoFilter is for a Table [Issue #3356](https://github.com/PHPOffice/PhpSpreadsheet/issues/3356) [PR #3358](https://github.com/PHPOffice/PhpSpreadsheet/pull/3358) and for Center Alignment of Headers [Issue #3395](https://github.com/PHPOffice/PhpSpreadsheet/issues/3395) [PR #3399](https://github.com/PHPOffice/PhpSpreadsheet/pull/3399) +- Decimal Precision for Scientific Number Format Mask [Issue #3381](https://github.com/PHPOffice/PhpSpreadsheet/issues/3381) [PR #3382](https://github.com/PHPOffice/PhpSpreadsheet/pull/3382) +- Xls Writer Parser Handle Boolean Literals as Function Arguments [Issue #3369](https://github.com/PHPOffice/PhpSpreadsheet/issues/3369) [PR #3391](https://github.com/PHPOffice/PhpSpreadsheet/pull/3391) +- Conditional Formatting Improvements for Xlsx [Issue #3370](https://github.com/PHPOffice/PhpSpreadsheet/issues/3370) [Issue #3202](https://github.com/PHPOffice/PhpSpreadsheet/issues/3302) [PR #3372](https://github.com/PHPOffice/PhpSpreadsheet/pull/3372) +- Coerce Bool to Int for Mathematical Operations on Arrays [Issue #3389](https://github.com/PHPOffice/PhpSpreadsheet/issues/3389) [Issue #3396](https://github.com/PHPOffice/PhpSpreadsheet/issues/3396) [PR #3392](https://github.com/PHPOffice/PhpSpreadsheet/pull/3392) + +## 1.27.1 - 2023-02-08 + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fix Composer --dev dependency issue with dealerdirect/phpcodesniffer-composer-installer renaming their `master` branch to `main` + + +## 1.27.0 - 2023-01-24 + +### Added + +- Option to specify a range of columns/rows for the Row/Column `isEmpty()` methods [PR #3315](https://github.com/PHPOffice/PhpSpreadsheet/pull/3315) +- Option for Cell Iterator to return a null value or create and return a new cell when accessing a cell that doesn't exist [PR #3314](https://github.com/PHPOffice/PhpSpreadsheet/pull/3314) +- Support for Structured References in the Calculation Engine [PR #3261](https://github.com/PHPOffice/PhpSpreadsheet/pull/3261) +- Limited Support for Form Controls [PR #3130](https://github.com/PHPOffice/PhpSpreadsheet/pull/3130) [Issue #2396](https://github.com/PHPOffice/PhpSpreadsheet/issues/2396) [Issue #1770](https://github.com/PHPOffice/PhpSpreadsheet/issues/1770) [Issue #2388](https://github.com/PHPOffice/PhpSpreadsheet/issues/2388) [Issue #2904](https://github.com/PHPOffice/PhpSpreadsheet/issues/2904) [Issue #2661](https://github.com/PHPOffice/PhpSpreadsheet/issues/2661) + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Shared/JAMA is removed. [PR #3260](https://github.com/PHPOffice/PhpSpreadsheet/pull/3260) + +### Fixed + +- Namespace-Aware Code for SheetViewOptions, SheetProtection [PR #3230](https://github.com/PHPOffice/PhpSpreadsheet/pull/3230) +- Additional Method for XIRR if Newton-Raphson Doesn't Converge [Issue #689](https://github.com/PHPOffice/PhpSpreadsheet/issues/689) [PR #3262](https://github.com/PHPOffice/PhpSpreadsheet/pull/3262) +- Better Handling of Composite Charts [Issue #2333](https://github.com/PHPOffice/PhpSpreadsheet/issues/2333) [PR #3265](https://github.com/PHPOffice/PhpSpreadsheet/pull/3265) +- Update Column Reference for Columns Beginning with Y and Z [Issue #3263](https://github.com/PHPOffice/PhpSpreadsheet/issues/3263) [PR #3264](https://github.com/PHPOffice/PhpSpreadsheet/pull/3264) +- Honor Fit to 1-Page Height Html/Pdf [Issue #3266](https://github.com/PHPOffice/PhpSpreadsheet/issues/3266) [PR #3279](https://github.com/PHPOffice/PhpSpreadsheet/pull/3279) +- AND/OR/XOR Handling of Literal Strings [PR #3287](https://github.com/PHPOffice/PhpSpreadsheet/pull/3287) +- Xls Reader Vertical Break and Writer Page Order [Issue #3305](https://github.com/PHPOffice/PhpSpreadsheet/issues/3305) [PR #3306](https://github.com/PHPOffice/PhpSpreadsheet/pull/3306) + + +## 1.26.0 - 2022-12-21 + +### Added + +- Extended flag options for the Reader `load()` and Writer `save()` methods +- Apply Row/Column limits (1048576 and XFD) in ReferenceHelper [PR #3213](https://github.com/PHPOffice/PhpSpreadsheet/pull/3213) +- Allow the creation of In-Memory Drawings from a string of binary image data, or from a stream. [PR #3157](https://github.com/PHPOffice/PhpSpreadsheet/pull/3157) +- Xlsx Reader support for Pivot Tables [PR #2829](https://github.com/PHPOffice/PhpSpreadsheet/pull/2829) +- Permit Date/Time Entered on Spreadsheet to be calculated as Float [Issue #1416](https://github.com/PHPOffice/PhpSpreadsheet/issues/1416) [PR #3121](https://github.com/PHPOffice/PhpSpreadsheet/pull/3121) + +### Changed + +- Nothing + +### Deprecated + +- Direct update of Calculation::suppressFormulaErrors is replaced with setter. +- Font public static variable defaultColumnWidths replaced with constant DEFAULT_COLUMN_WIDTHS. +- ExcelError public static variable errorCodes replaced with constant ERROR_CODES. +- NumberFormat constant FORMAT_DATE_YYYYMMDD2 replaced with existing identical FORMAT_DATE_YYYYMMDD. + +### Removed + +- Nothing + +### Fixed + +- Fixed handling for `_xlws` prefixed functions from Office365 [Issue #3245](https://github.com/PHPOffice/PhpSpreadsheet/issues/3245) [PR #3247](https://github.com/PHPOffice/PhpSpreadsheet/pull/3247) +- Conditionals formatting rules applied to rows/columns are removed [Issue #3184](https://github.com/PHPOffice/PhpSpreadsheet/issues/3184) [PR #3213](https://github.com/PHPOffice/PhpSpreadsheet/pull/3213) +- Treat strings containing currency or accounting values as floats in Calculation Engine operations [Issue #3165](https://github.com/PHPOffice/PhpSpreadsheet/issues/3165) [PR #3189](https://github.com/PHPOffice/PhpSpreadsheet/pull/3189) +- Treat strings containing percentage values as floats in Calculation Engine operations [Issue #3155](https://github.com/PHPOffice/PhpSpreadsheet/issues/3155) [PR #3156](https://github.com/PHPOffice/PhpSpreadsheet/pull/3156) and [PR #3164](https://github.com/PHPOffice/PhpSpreadsheet/pull/3164) +- Xlsx Reader Accept Palette of Fewer than 64 Colors [Issue #3093](https://github.com/PHPOffice/PhpSpreadsheet/issues/3093) [PR #3096](https://github.com/PHPOffice/PhpSpreadsheet/pull/3096) +- Use Locale-Independent Float Conversion for Xlsx Writer Custom Property [Issue #3095](https://github.com/PHPOffice/PhpSpreadsheet/issues/3095) [PR #3099](https://github.com/PHPOffice/PhpSpreadsheet/pull/3099) +- Allow setting AutoFilter range on a single cell or row [Issue #3102](https://github.com/PHPOffice/PhpSpreadsheet/issues/3102) [PR #3111](https://github.com/PHPOffice/PhpSpreadsheet/pull/3111) +- Xlsx Reader External Data Validations Flag Missing [Issue #2677](https://github.com/PHPOffice/PhpSpreadsheet/issues/2677) [PR #3078](https://github.com/PHPOffice/PhpSpreadsheet/pull/3078) +- Reduces extra memory usage on `__destruct()` calls [PR #3092](https://github.com/PHPOffice/PhpSpreadsheet/pull/3092) +- Additional properties for Trendlines [Issue #3011](https://github.com/PHPOffice/PhpSpreadsheet/issues/3011) [PR #3028](https://github.com/PHPOffice/PhpSpreadsheet/pull/3028) +- Calculation suppressFormulaErrors fix [Issue #1531](https://github.com/PHPOffice/PhpSpreadsheet/issues/1531) [PR #3092](https://github.com/PHPOffice/PhpSpreadsheet/pull/3092) +- Permit Date/Time Entered on Spreadsheet to be Calculated as Float [Issue #1416](https://github.com/PHPOffice/PhpSpreadsheet/issues/1416) [PR #3121](https://github.com/PHPOffice/PhpSpreadsheet/pull/3121) +- Incorrect Handling of Data Validation Formula Containing Ampersand [Issue #3145](https://github.com/PHPOffice/PhpSpreadsheet/issues/3145) [PR #3146](https://github.com/PHPOffice/PhpSpreadsheet/pull/3146) +- Xlsx Namespace Handling of Drawings, RowAndColumnAttributes, MergeCells [Issue #3138](https://github.com/PHPOffice/PhpSpreadsheet/issues/3138) [PR #3136](https://github.com/PHPOffice/PhpSpreadsheet/pull/3137) +- Generation3 Copy With Image in Footer [Issue #3126](https://github.com/PHPOffice/PhpSpreadsheet/issues/3126) [PR #3140](https://github.com/PHPOffice/PhpSpreadsheet/pull/3140) +- MATCH Function Problems with Int/Float Compare and Wildcards [Issue #3141](https://github.com/PHPOffice/PhpSpreadsheet/issues/3141) [PR #3142](https://github.com/PHPOffice/PhpSpreadsheet/pull/3142) +- Fix ODS Read Filter on number-columns-repeated cell [Issue #3148](https://github.com/PHPOffice/PhpSpreadsheet/issues/3148) [PR #3149](https://github.com/PHPOffice/PhpSpreadsheet/pull/3149) +- Problems Formatting Very Small and Very Large Numbers [Issue #3128](https://github.com/PHPOffice/PhpSpreadsheet/issues/3128) [PR #3152](https://github.com/PHPOffice/PhpSpreadsheet/pull/3152) +- XlsxWrite preserve line styles for y-axis, not just x-axis [PR #3163](https://github.com/PHPOffice/PhpSpreadsheet/pull/3163) +- Xlsx Namespace Handling of Drawings, RowAndColumnAttributes, MergeCells [Issue #3138](https://github.com/PHPOffice/PhpSpreadsheet/issues/3138) [PR #3137](https://github.com/PHPOffice/PhpSpreadsheet/pull/3137) +- More Detail for Cyclic Error Messages [Issue #3169](https://github.com/PHPOffice/PhpSpreadsheet/issues/3169) [PR #3170](https://github.com/PHPOffice/PhpSpreadsheet/pull/3170) +- Improved Documentation for Deprecations - many PRs [Issue #3162](https://github.com/PHPOffice/PhpSpreadsheet/issues/3162) + + +## 1.25.2 - 2022-09-25 + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Composer dependency clash with ezyang/htmlpurifier + + +## 1.25.0 - 2022-09-25 + +### Added + +- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions +- Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions +- Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of + JpGraph library to render charts added. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes. + +### Changed + +- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) + - Merge methods now allow an additional `$behaviour` argument. Permitted values are: + - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) + - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell + +### Deprecated + +- Axis getLineProperty deprecated in favor of getLineColorProperty. +- Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated. +- Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated. +- ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed). +- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT. +- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead. + +### Removed + +- Nothing + +### Fixed + +- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077) +- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) +- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) +- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) +- cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) +- Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) +- Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960) +- Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971) +- Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950) +- Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976) +- Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) +- Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) +- Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) +- Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016) +- Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018) +- Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048) +- R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052) +- Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053) + +## 1.24.1 - 2022-07-18 + +### Added + +- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0 +- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) +- Additional Chart fixes + - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) + - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) + - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) +- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) + +## 1.24.0 - 2022-07-09 + +Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. + +### Added + +- Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) +- Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) +- Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) + + Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. + +- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853) +- Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) +- Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) +- Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) +- Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty": + - Treat rows/columns containing no cell records as empty (default) + - Treat cells containing a null value as empty + - Treat cells containing an empty string as empty + +### Changed + +- Modify `rangeBoundaries()`, `rangeDimension()` and `getRangeBoundaries()` Coordinate methods to work with row/column ranges as well as with cell ranges and cells [PR #2926](https://github.com/PHPOffice/PhpSpreadsheet/pull/2926) +- Better enforcement of value modification to match specified datatype when using `setValueExplicit()` +- Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776) +- Memory and speed improvements, particularly for the Cell Collection, and the Writers. + + See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions +- Improved performance for removing rows/columns from a worksheet + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) +- Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) +- Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) +- Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) +- Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) +- Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) [PR #2923](https://github.com/PHPOffice/PhpSpreadsheet/pull/2923) +- Adjust both coordinates for two-cell anchors when rows/columns are added/deleted. [Issue #2908](https://github.com/PHPOffice/PhpSpreadsheet/issues/2908) [PR #2909](https://github.com/PHPOffice/PhpSpreadsheet/pull/2909) +- Keep calculated string results below 32K. [PR #2921](https://github.com/PHPOffice/PhpSpreadsheet/pull/2921) +- Filter out illegal Unicode char values FFFE/FFFF. [Issue #2897](https://github.com/PHPOffice/PhpSpreadsheet/issues/2897) [PR #2910](https://github.com/PHPOffice/PhpSpreadsheet/pull/2910) +- Better handling of REF errors and propagation of all errors in Calculation engine. [PR #2902](https://github.com/PHPOffice/PhpSpreadsheet/pull/2902) +- Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) + +## 1.23.0 - 2022-04-24 + +### Added + +- Ods Writer support for Freeze Pane [Issue #2013](https://github.com/PHPOffice/PhpSpreadsheet/issues/2013) [PR #2755](https://github.com/PHPOffice/PhpSpreadsheet/pull/2755) +- Ods Writer support for setting column width/row height (including the use of AutoSize) [Issue #2346](https://github.com/PHPOffice/PhpSpreadsheet/issues/2346) [PR #2753](https://github.com/PHPOffice/PhpSpreadsheet/pull/2753) +- Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts. +- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions. +- Implementation of the ISREF() Information function. +- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. + + (i.e a value of "12,345.67" can be read as numeric `12345.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled. + + This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. + +- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) [#2674](https://github.com/PHPOffice/PhpSpreadsheet/pull/2674) +- Limited support for Xls Reader to handle Conditional Formatting: + + Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color. + +- Add ability to suppress Mac line ending check for CSV [#2623](https://github.com/PHPOffice/PhpSpreadsheet/pull/2623) +- Initial support for creating and writing Tables (Xlsx Writer only) [PR #2671](https://github.com/PHPOffice/PhpSpreadsheet/pull/2671) + + See `/samples/Table` for examples of use. + + Note that PreCalculateFormulas needs to be disabled when saving spreadsheets containing tables with formulae (totals or column formulae). + +### Changed + +- Gnumeric Reader now loads number formatting for cells. +- Gnumeric Reader now correctly identifies selected worksheet and selected cells in a worksheet. +- Some Refactoring of the Ods Reader, moving all formula and address translation from Ods to Excel into a separate class to eliminate code duplication and ensure consistency. +- Make Boolean Conversion in Csv Reader locale-aware when using the String Value Binder. + + This is determined by the Calculation Engine locale setting. + + (i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.) +- Allow `psr/simple-cache` 2.x + +### Deprecated + +- All Excel Function implementations in `Calculation\Functions` (including the Error functions) have been moved to dedicated classes for groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. +- Worksheet methods that reference cells "byColumnandRow". All such methods have an equivalent that references the cell by its address (e.g. '`E3'` rather than `5, 3`). + + These functions now accept either a cell address string (`'E3')` or an array with columnId and rowId (`[5, 3]`) or a new `CellAddress` object as their `cellAddress`/`coordinate` argument. + This includes the methods: + - `setCellValueByColumnAndRow()` use the equivalent `setCellValue()` + - `setCellValueExplicitByColumnAndRow()` use the equivalent `setCellValueExplicit()` + - `getCellByColumnAndRow()` use the equivalent `getCell()` + - `cellExistsByColumnAndRow()` use the equivalent `cellExists()` + - `getStyleByColumnAndRow()` use the equivalent `getStyle()` + - `setBreakByColumnAndRow()` use the equivalent `setBreak()` + - `mergeCellsByColumnAndRow()` use the equivalent `mergeCells()` + - `unmergeCellsByColumnAndRow()` use the equivalent `unmergeCells()` + - `protectCellsByColumnAndRow()` use the equivalent `protectCells()` + - `unprotectCellsByColumnAndRow()` use the equivalent `unprotectCells()` + - `setAutoFilterByColumnAndRow()` use the equivalent `setAutoFilter()` + - `freezePaneByColumnAndRow()` use the equivalent `freezePane()` + - `getCommentByColumnAndRow()` use the equivalent `getComment()` + - `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()` + + This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method); + and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument. + +### Removed + +- Nothing + +### Fixed + +- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754) +- Support for "chained" ranges (e.g. `A5:C10:C20:F1`) in the Calculation Engine; and also support for using named ranges with the Range operator (e.g. `NamedRange1:NamedRange2`) [Issue #2730](https://github.com/PHPOffice/PhpSpreadsheet/issues/2730) [PR #2746](https://github.com/PHPOffice/PhpSpreadsheet/pull/2746) +- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689) +- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) +- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. +- Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. +- Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. +- Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) +- Fixed behaviour of XLSX font style vertical align settings [PR #2619](https://github.com/PHPOffice/PhpSpreadsheet/pull/2619) +- Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. + + Note that this method is used when translating Excel functions between `en_us` and other locale languages, as well as when converting formulae between different spreadsheet formats (e.g. Ods to Excel). + + Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic. +- Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651) +- Change open mode for output from `wb+` to `wb` [Issue #2372](https://github.com/PHPOffice/PhpSpreadsheet/issues/2372) [PR #2657](https://github.com/PHPOffice/PhpSpreadsheet/pull/2657) +- Use color palette if supplied [Issue #2499](https://github.com/PHPOffice/PhpSpreadsheet/issues/2499) [PR #2595](https://github.com/PHPOffice/PhpSpreadsheet/pull/2595) +- Xls reader treat drawing offsets as int rather than float [PR #2648](https://github.com/PHPOffice/PhpSpreadsheet/pull/2648) +- Handle booleans in conditional styles properly [PR #2654](https://github.com/PHPOffice/PhpSpreadsheet/pull/2654) +- Fix for reading files in the root directory of a ZipFile, which should not be prefixed by relative paths ("./") as dirname($filename) does by default. +- Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739) +- Time Interval Formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) + +## 1.22.0 - 2022-02-18 + +### Added + +- Namespacing phase 2 - styles. +[PR #2471](https://github.com/PHPOffice/PhpSpreadsheet/pull/2471) + +- Improved support for passing of array arguments to Excel function implementations to return array results (where appropriate). [Issue #2551](https://github.com/PHPOffice/PhpSpreadsheet/issues/2551) + + This is the first stage in an ongoing process of adding array support to all appropriate function implementations, +- Support for the Excel365 Math/Trig SEQUENCE() function [PR #2536](https://github.com/PHPOffice/PhpSpreadsheet/pull/2536) +- Support for the Excel365 Math/Trig RANDARRAY() function [PR #2540](https://github.com/PHPOffice/PhpSpreadsheet/pull/2540) + + Note that the Spill Operator is not yet supported in the Calculation Engine; but this can still be useful for defining array constants. +- Improved support for Conditional Formatting Rules [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491) + - Provide support for a wider range of Conditional Formatting Rules for Xlsx Reader/Writer: + - Cells Containing (cellIs) + - Specific Text (containing, notContaining, beginsWith, endsWith) + - Dates Occurring (all supported timePeriods) + - Blanks/NoBlanks + - Errors/NoErrors + - Duplicates/Unique + - Expression + - Provision of CF Wizards (for all the above listed rule types) to help create/modify CF Rules without having to manage all the combinations of types/operators, and the complexities of formula expressions, or the text/timePeriod attributes. + + See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/conditional-formatting/) for details + + - Full support of the above CF Rules for the Xlsx Reader and Writer; even when the file being loaded has CF rules listed in the `` element for the worksheet rather than the `` element. + - Provision of a CellMatcher to identify if rules are matched for a cell, and which matching style will be applied. + - Improved documentation and examples, covering all supported CF rule types. + - Add support for one digit decimals (FORMAT_NUMBER_0, FORMAT_PERCENTAGE_0). [PR #2525](https://github.com/PHPOffice/PhpSpreadsheet/pull/2525) + - Initial work enabling Excel function implementations for handling arrays as arguments when used in "array formulae" [#2562](https://github.com/PHPOffice/PhpSpreadsheet/issues/2562) + - Enable most of the Date/Time functions to accept array arguments [#2573](https://github.com/PHPOffice/PhpSpreadsheet/issues/2573) + - Array ready functions - Text, Math/Trig, Statistical, Engineering and Logical [#2580](https://github.com/PHPOffice/PhpSpreadsheet/issues/2580) + +### Changed + +- Additional Russian translations for Excel Functions (courtesy of aleks-samurai). +- Improved code coverage for NumberFormat. [PR #2556](https://github.com/PHPOffice/PhpSpreadsheet/pull/2556) +- Extract some methods from the Calculation Engine into dedicated classes [#2537](https://github.com/PHPOffice/PhpSpreadsheet/issues/2537) +- Eliminate calls to `flattenSingleValue()` that are no longer required when we're checking for array values as arguments [#2590](https://github.com/PHPOffice/PhpSpreadsheet/issues/2590) + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fixed `ReferenceHelper@insertNewBefore` behavior when removing column before last column with null value [PR #2541](https://github.com/PHPOffice/PhpSpreadsheet/pull/2541) +- Fix bug with `DOLLARDE()` and `DOLLARFR()` functions when the dollar value is negative [Issue #2578](https://github.com/PHPOffice/PhpSpreadsheet/issues/2578) [PR #2579](https://github.com/PHPOffice/PhpSpreadsheet/pull/2579) +- Fix partial function name matching when translating formulae from Russian to English [Issue #2533](https://github.com/PHPOffice/PhpSpreadsheet/issues/2533) [PR #2534](https://github.com/PHPOffice/PhpSpreadsheet/pull/2534) +- Various bugs related to Conditional Formatting Rules, and errors in the Xlsx Writer for Conditional Formatting [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491) +- Xlsx Reader merge range fixes. [Issue #2501](https://github.com/PHPOffice/PhpSpreadsheet/issues/2501) [PR #2504](https://github.com/PHPOffice/PhpSpreadsheet/pull/2504) +- Handle explicit "date" type for Cell in Xlsx Reader. [Issue #2373](https://github.com/PHPOffice/PhpSpreadsheet/issues/2373) [PR #2485](https://github.com/PHPOffice/PhpSpreadsheet/pull/2485) +- Recalibrate Row/Column Dimensions after removeRow/Column. [Issue #2442](https://github.com/PHPOffice/PhpSpreadsheet/issues/2442) [PR #2486](https://github.com/PHPOffice/PhpSpreadsheet/pull/2486) +- Refinement for XIRR. [Issue #2469](https://github.com/PHPOffice/PhpSpreadsheet/issues/2469) [PR #2487](https://github.com/PHPOffice/PhpSpreadsheet/pull/2487) +- Xlsx Reader handle cell with non-null explicit type but null value. [Issue #2488](https://github.com/PHPOffice/PhpSpreadsheet/issues/2488) [PR #2489](https://github.com/PHPOffice/PhpSpreadsheet/pull/2489) +- Xlsx Reader fix height and width for oneCellAnchorDrawings. [PR #2492](https://github.com/PHPOffice/PhpSpreadsheet/pull/2492) +- Fix rounding error in NumberFormat::NUMBER_PERCENTAGE, NumberFormat::NUMBER_PERCENTAGE_00. [PR #2555](https://github.com/PHPOffice/PhpSpreadsheet/pull/2555) +- Don't treat thumbnail file as xml. [Issue #2516](https://github.com/PHPOffice/PhpSpreadsheet/issues/2516) [PR #2517](https://github.com/PHPOffice/PhpSpreadsheet/pull/2517) +- Eliminating Xlsx Reader warning when no sz tag for RichText. [Issue #2542](https://github.com/PHPOffice/PhpSpreadsheet/issues/2542) [PR #2550](https://github.com/PHPOffice/PhpSpreadsheet/pull/2550) +- Fix Xlsx/Xls Writer handling of inline strings. [Issue #353](https://github.com/PHPOffice/PhpSpreadsheet/issues/353) [PR #2569](https://github.com/PHPOffice/PhpSpreadsheet/pull/2569) +- Richtext colors were not being read correctly after namespace change [#2458](https://github.com/PHPOffice/PhpSpreadsheet/issues/2458) +- Fix discrepancy between the way markdown tables are rendered in ReadTheDocs and in PHPStorm [#2520](https://github.com/PHPOffice/PhpSpreadsheet/issues/2520) +- Update Russian Functions Text File [#2557](https://github.com/PHPOffice/PhpSpreadsheet/issues/2557) +- Fix documentation, instantiation example [#2564](https://github.com/PHPOffice/PhpSpreadsheet/issues/2564) + + +## 1.21.0 - 2022-01-06 + +### Added + +- Ability to add a picture to the background of the comment. Supports four image formats: png, jpeg, gif, bmp. New `Comment::setSizeAsBackgroundImage()` to change the size of a comment to the size of a background image. [Issue #1547](https://github.com/PHPOffice/PhpSpreadsheet/issues/1547) [PR #2422](https://github.com/PHPOffice/PhpSpreadsheet/pull/2422) +- Ability to set default paper size and orientation [PR #2410](https://github.com/PHPOffice/PhpSpreadsheet/pull/2410) +- Ability to extend AutoFilter to Maximum Row [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414) + +### Changed + +- Xlsx Writer will evaluate AutoFilter only if it is as yet unevaluated, or has changed since it was last evaluated [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414) + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Rounding in `NumberFormatter` [Issue #2385](https://github.com/PHPOffice/PhpSpreadsheet/issues/2385) [PR #2399](https://github.com/PHPOffice/PhpSpreadsheet/pull/2399) +- Support for themes [Issue #2075](https://github.com/PHPOffice/PhpSpreadsheet/issues/2075) [Issue #2387](https://github.com/PHPOffice/PhpSpreadsheet/issues/2387) [PR #2403](https://github.com/PHPOffice/PhpSpreadsheet/pull/2403) +- Read spreadsheet with `#` in name [Issue #2405](https://github.com/PHPOffice/PhpSpreadsheet/issues/2405) [PR #2409](https://github.com/PHPOffice/PhpSpreadsheet/pull/2409) +- Improve PDF support for page size and orientation [Issue #1691](https://github.com/PHPOffice/PhpSpreadsheet/issues/1691) [PR #2410](https://github.com/PHPOffice/PhpSpreadsheet/pull/2410) +- Wildcard handling issues in text match [Issue #2430](https://github.com/PHPOffice/PhpSpreadsheet/issues/2430) [PR #2431](https://github.com/PHPOffice/PhpSpreadsheet/pull/2431) +- Respect DataType in `insertNewBefore` [PR #2433](https://github.com/PHPOffice/PhpSpreadsheet/pull/2433) +- Handle rows explicitly hidden after AutoFilter [Issue #1641](https://github.com/PHPOffice/PhpSpreadsheet/issues/1641) [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414) +- Special characters in image file name [Issue #1470](https://github.com/PHPOffice/PhpSpreadsheet/issues/1470) [Issue #2415](https://github.com/PHPOffice/PhpSpreadsheet/issues/2415) [PR #2416](https://github.com/PHPOffice/PhpSpreadsheet/pull/2416) +- Mpdf with very many styles [Issue #2432](https://github.com/PHPOffice/PhpSpreadsheet/issues/2432) [PR #2434](https://github.com/PHPOffice/PhpSpreadsheet/pull/2434) +- Name clashes between parsed and unparsed drawings [Issue #1767](https://github.com/PHPOffice/PhpSpreadsheet/issues/1767) [Issue #2396](https://github.com/PHPOffice/PhpSpreadsheet/issues/2396) [PR #2423](https://github.com/PHPOffice/PhpSpreadsheet/pull/2423) +- Fill pattern start and end colors [Issue #2441](https://github.com/PHPOffice/PhpSpreadsheet/issues/2441) [PR #2444](https://github.com/PHPOffice/PhpSpreadsheet/pull/2444) +- General style specified in wrong case [Issue #2450](https://github.com/PHPOffice/PhpSpreadsheet/issues/2450) [PR #2451](https://github.com/PHPOffice/PhpSpreadsheet/pull/2451) +- Null passed to `AutoFilter::setRange()` [Issue #2281](https://github.com/PHPOffice/PhpSpreadsheet/issues/2281) [PR #2454](https://github.com/PHPOffice/PhpSpreadsheet/pull/2454) +- Another undefined index in Xls reader (#2470) [Issue #2463](https://github.com/PHPOffice/PhpSpreadsheet/issues/2463) [PR #2470](https://github.com/PHPOffice/PhpSpreadsheet/pull/2470) +- Allow single-cell checks on conditional styles, even when the style is configured for a range of cells (#) [PR #2483](https://github.com/PHPOffice/PhpSpreadsheet/pull/2483) + +## 1.20.0 - 2021-11-23 + +### Added + +- Xlsx Writer Support for WMF Files [#2339](https://github.com/PHPOffice/PhpSpreadsheet/issues/2339) +- Use standard temporary file for internal use of HTMLPurifier [#2383](https://github.com/PHPOffice/PhpSpreadsheet/issues/2383) + +### Changed + +- Drop support for PHP 7.2, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support +- Use native typing for objects that were already documented as such + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fixed null conversation for strToUpper [#2292](https://github.com/PHPOffice/PhpSpreadsheet/issues/2292) +- Fixed Trying to access array offset on value of type null (Xls Reader) [#2315](https://github.com/PHPOffice/PhpSpreadsheet/issues/2315) +- Don't corrupt XLSX files containing data validation [#2377](https://github.com/PHPOffice/PhpSpreadsheet/issues/2377) +- Non-fixed cells were not updated if shared formula has a fixed cell [#2354](https://github.com/PHPOffice/PhpSpreadsheet/issues/2354) +- Declare key of generic ArrayObject +- CSV reader better support for boolean values [#2374](https://github.com/PHPOffice/PhpSpreadsheet/pull/2374) +- Some ZIP file could not be read [#2376](https://github.com/PHPOffice/PhpSpreadsheet/pull/2376) +- Fix regression were hyperlinks could not be read [#2391](https://github.com/PHPOffice/PhpSpreadsheet/pull/2391) +- AutoFilter Improvements [#2393](https://github.com/PHPOffice/PhpSpreadsheet/pull/2393) +- Don't corrupt file when using chart with fill color [#589](https://github.com/PHPOffice/PhpSpreadsheet/pull/589) +- Restore imperfect array formula values in xlsx writer [#2343](https://github.com/PHPOffice/PhpSpreadsheet/pull/2343) +- Restore explicit list of changes to PHPExcel migration document [#1546](https://github.com/PHPOffice/PhpSpreadsheet/issues/1546) + +## 1.19.0 - 2021-10-31 + +### Added + +- Ability to set style on named range, and validate input to setSelectedCells [Issue #2279](https://github.com/PHPOffice/PhpSpreadsheet/issues/2279) [PR #2280](https://github.com/PHPOffice/PhpSpreadsheet/pull/2280) +- Process comments in Sylk file [Issue #2276](https://github.com/PHPOffice/PhpSpreadsheet/issues/2276) [PR #2277](https://github.com/PHPOffice/PhpSpreadsheet/pull/2277) +- Addition of Custom Properties to Ods Writer, and 32-bit-safe timestamps for Document Properties [PR #2113](https://github.com/PHPOffice/PhpSpreadsheet/pull/2113) +- Added callback to CSV reader to set user-specified defaults for various properties (especially for escape which has a poor PHP-inherited default of backslash which does not correspond with Excel) [PR #2103](https://github.com/PHPOffice/PhpSpreadsheet/pull/2103) +- Phase 1 of better namespace handling for Xlsx, resolving many open issues [PR #2173](https://github.com/PHPOffice/PhpSpreadsheet/pull/2173) [PR #2204](https://github.com/PHPOffice/PhpSpreadsheet/pull/2204) [PR #2303](https://github.com/PHPOffice/PhpSpreadsheet/pull/2303) +- Add ability to extract images if source is a URL [Issue #1997](https://github.com/PHPOffice/PhpSpreadsheet/issues/1997) [PR #2072](https://github.com/PHPOffice/PhpSpreadsheet/pull/2072) +- Support for passing flags in the Reader `load()` and Writer `save()`methods, and through the IOFactory, to set behaviours [PR #2136](https://github.com/PHPOffice/PhpSpreadsheet/pull/2136) + - See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#readerwriter-flags) for details +- More flexibility in the StringValueBinder to determine what datatypes should be treated as strings [PR #2138](https://github.com/PHPOffice/PhpSpreadsheet/pull/2138) +- Helper class for conversion between css size Units of measure (`px`, `pt`, `pc`, `in`, `cm`, `mm`) [PR #2152](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145) +- Allow Row height and Column Width to be set using different units of measure (`px`, `pt`, `pc`, `in`, `cm`, `mm`), rather than only in points or MS Excel column width units [PR #2152](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145) +- Ability to stream to an Amazon S3 bucket [Issue #2249](https://github.com/PHPOffice/PhpSpreadsheet/issues/2249) +- Provided a Size Helper class to validate size values (pt, px, em) [PR #1694](https://github.com/PHPOffice/PhpSpreadsheet/pull/1694) + +### Changed + +- Nothing. + +### Deprecated + +- PHP 8.1 will deprecate auto_detect_line_endings. As a result of this change, Csv Reader using some release after PHP8.1 will no longer be able to handle a Csv with Mac line endings. + +### Removed + +- Nothing. + +### Fixed + +- Unexpected format in Xlsx Timestamp [Issue #2331](https://github.com/PHPOffice/PhpSpreadsheet/issues/2331) [PR #2332](https://github.com/PHPOffice/PhpSpreadsheet/pull/2332) +- Corrections for HLOOKUP [Issue #2123](https://github.com/PHPOffice/PhpSpreadsheet/issues/2123) [PR #2330](https://github.com/PHPOffice/PhpSpreadsheet/pull/2330) +- Corrections for Xlsx Read Comments [Issue #2316](https://github.com/PHPOffice/PhpSpreadsheet/issues/2316) [PR #2329](https://github.com/PHPOffice/PhpSpreadsheet/pull/2329) +- Lowercase Calibri font names [Issue #2273](https://github.com/PHPOffice/PhpSpreadsheet/issues/2273) [PR #2325](https://github.com/PHPOffice/PhpSpreadsheet/pull/2325) +- isFormula Referencing Sheet with Space in Title [Issue #2304](https://github.com/PHPOffice/PhpSpreadsheet/issues/2304) [PR #2306](https://github.com/PHPOffice/PhpSpreadsheet/pull/2306) +- Xls Reader Fatal Error due to Undefined Offset [Issue #1114](https://github.com/PHPOffice/PhpSpreadsheet/issues/1114) [PR #2308](https://github.com/PHPOffice/PhpSpreadsheet/pull/2308) +- Permit Csv Reader delimiter to be set to null [Issue #2287](https://github.com/PHPOffice/PhpSpreadsheet/issues/2287) [PR #2288](https://github.com/PHPOffice/PhpSpreadsheet/pull/2288) +- Csv Reader did not handle booleans correctly [PR #2232](https://github.com/PHPOffice/PhpSpreadsheet/pull/2232) +- Problems when deleting sheet with local defined name [Issue #2266](https://github.com/PHPOffice/PhpSpreadsheet/issues/2266) [PR #2284](https://github.com/PHPOffice/PhpSpreadsheet/pull/2284) +- Worksheet passwords were not always handled correctly [Issue #1897](https://github.com/PHPOffice/PhpSpreadsheet/issues/1897) [PR #2197](https://github.com/PHPOffice/PhpSpreadsheet/pull/2197) +- Gnumeric Reader will now distinguish between Created and Modified timestamp [PR #2133](https://github.com/PHPOffice/PhpSpreadsheet/pull/2133) +- Xls Reader will now handle MACCENTRALEUROPE with or without hyphen [Issue #549](https://github.com/PHPOffice/PhpSpreadsheet/issues/549) [PR #2213](https://github.com/PHPOffice/PhpSpreadsheet/pull/2213) +- Tweaks to input file validation [Issue #1718](https://github.com/PHPOffice/PhpSpreadsheet/issues/1718) [PR #2217](https://github.com/PHPOffice/PhpSpreadsheet/pull/2217) +- Html Reader did not handle comments correctly [Issue #2234](https://github.com/PHPOffice/PhpSpreadsheet/issues/2234) [PR #2235](https://github.com/PHPOffice/PhpSpreadsheet/pull/2235) +- Apache OpenOffice Uses Unexpected Case for General format [Issue #2239](https://github.com/PHPOffice/PhpSpreadsheet/issues/2239) [PR #2242](https://github.com/PHPOffice/PhpSpreadsheet/pull/2242) +- Problems with fraction formatting [Issue #2253](https://github.com/PHPOffice/PhpSpreadsheet/issues/2253) [PR #2254](https://github.com/PHPOffice/PhpSpreadsheet/pull/2254) +- Xlsx Reader had problems reading file with no styles.xml or empty styles.xml [Issue #2246](https://github.com/PHPOffice/PhpSpreadsheet/issues/2246) [PR #2247](https://github.com/PHPOffice/PhpSpreadsheet/pull/2247) +- Xlsx Reader did not read Data Validation flags correctly [Issue #2224](https://github.com/PHPOffice/PhpSpreadsheet/issues/2224) [PR #2225](https://github.com/PHPOffice/PhpSpreadsheet/pull/2225) +- Better handling of empty arguments in Calculation engine [PR #2143](https://github.com/PHPOffice/PhpSpreadsheet/pull/2143) +- Many fixes for Autofilter [Issue #2216](https://github.com/PHPOffice/PhpSpreadsheet/issues/2216) [PR #2141](https://github.com/PHPOffice/PhpSpreadsheet/pull/2141) [PR #2162](https://github.com/PHPOffice/PhpSpreadsheet/pull/2162) [PR #2218](https://github.com/PHPOffice/PhpSpreadsheet/pull/2218) +- Locale generator will now use Unix line endings even on Windows [Issue #2172](https://github.com/PHPOffice/PhpSpreadsheet/issues/2172) [PR #2174](https://github.com/PHPOffice/PhpSpreadsheet/pull/2174) +- Support differences in implementation of Text functions between Excel/Ods/Gnumeric [PR #2151](https://github.com/PHPOffice/PhpSpreadsheet/pull/2151) +- Fixes to places where PHP8.1 enforces new or previously unenforced restrictions [PR #2137](https://github.com/PHPOffice/PhpSpreadsheet/pull/2137) [PR #2191](https://github.com/PHPOffice/PhpSpreadsheet/pull/2191) [PR #2231](https://github.com/PHPOffice/PhpSpreadsheet/pull/2231) +- Clone for HashTable was incorrect [PR #2130](https://github.com/PHPOffice/PhpSpreadsheet/pull/2130) +- Xlsx Reader was not evaluating Document Security Lock correctly [PR #2128](https://github.com/PHPOffice/PhpSpreadsheet/pull/2128) +- Error in COUPNCD handling end of month [Issue #2116](https://github.com/PHPOffice/PhpSpreadsheet/issues/2116) [PR #2119](https://github.com/PHPOffice/PhpSpreadsheet/pull/2119) +- Xls Writer Parser did not handle concatenation operator correctly [PR #2080](https://github.com/PHPOffice/PhpSpreadsheet/pull/2080) +- Xlsx Writer did not handle boolean false correctly [Issue #2082](https://github.com/PHPOffice/PhpSpreadsheet/issues/2082) [PR #2087](https://github.com/PHPOffice/PhpSpreadsheet/pull/2087) +- SUM needs to treat invalid strings differently depending on whether they come from a cell or are used as literals [Issue #2042](https://github.com/PHPOffice/PhpSpreadsheet/issues/2042) [PR #2045](https://github.com/PHPOffice/PhpSpreadsheet/pull/2045) +- Html reader could have set illegal coordinates when dealing with embedded tables [Issue #2029](https://github.com/PHPOffice/PhpSpreadsheet/issues/2029) [PR #2032](https://github.com/PHPOffice/PhpSpreadsheet/pull/2032) +- Documentation for printing gridlines was wrong [PR #2188](https://github.com/PHPOffice/PhpSpreadsheet/pull/2188) +- Return Value Error - DatabaseAbstruct::buildQuery() return null but must be string [Issue #2158](https://github.com/PHPOffice/PhpSpreadsheet/issues/2158) [PR #2160](https://github.com/PHPOffice/PhpSpreadsheet/pull/2160) +- Xlsx reader not recognize data validations that references another sheet [Issue #1432](https://github.com/PHPOffice/PhpSpreadsheet/issues/1432) [Issue #2149](https://github.com/PHPOffice/PhpSpreadsheet/issues/2149) [PR #2150](https://github.com/PHPOffice/PhpSpreadsheet/pull/2150) [PR #2265](https://github.com/PHPOffice/PhpSpreadsheet/pull/2265) +- Don't calculate cell width for autosize columns if a cell contains a null or empty string value [Issue #2165](https://github.com/PHPOffice/PhpSpreadsheet/issues/2165) [PR #2167](https://github.com/PHPOffice/PhpSpreadsheet/pull/2167) +- Allow negative interest rate values in a number of the Financial functions (`PPMT()`, `PMT()`, `FV()`, `PV()`, `NPER()`, etc) [Issue #2163](https://github.com/PHPOffice/PhpSpreadsheet/issues/2163) [PR #2164](https://github.com/PHPOffice/PhpSpreadsheet/pull/2164) +- Xls Reader changing grey background to black in Excel template [Issue #2147](https://github.com/PHPOffice/PhpSpreadsheet/issues/2147) [PR #2156](https://github.com/PHPOffice/PhpSpreadsheet/pull/2156) +- Column width and Row height styles in the Html Reader when the value includes a unit of measure [Issue #2145](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145). +- Data Validation flags not set correctly when reading XLSX files [Issue #2224](https://github.com/PHPOffice/PhpSpreadsheet/issues/2224) [PR #2225](https://github.com/PHPOffice/PhpSpreadsheet/pull/2225) +- Reading XLSX files without styles.xml throws an exception [Issue #2246](https://github.com/PHPOffice/PhpSpreadsheet/issues/2246) +- Improved performance of `Style::applyFromArray()` when applied to several cells [PR #1785](https://github.com/PHPOffice/PhpSpreadsheet/issues/1785). +- Improve XLSX parsing speed if no readFilter is applied (again) - [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772) + +## 1.18.0 - 2021-05-31 + +### Added + +- Enhancements to CSV Reader, allowing options to be set when using `IOFactory::load()` with a callback to set delimiter, enclosure, charset etc [PR #2103](https://github.com/PHPOffice/PhpSpreadsheet/pull/2103) - See [documentation](https://github.com/PHPOffice/PhpSpreadsheet/blob/master/docs/topics/reading-and-writing-to-file.md#csv-comma-separated-values) for details. +- Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053) +- Implemented basic AutoFiltering for Gnumeric Reader [PR #2055](https://github.com/PHPOffice/PhpSpreadsheet/pull/2055) +- Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028) +- Implemented URLENCODE() Web Function +- Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions. +- Support for ActiveSheet and SelectedCells in the ODS Reader and Writer [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) +- Support for notContainsText Conditional Style in xlsx [Issue #984](https://github.com/PHPOffice/PhpSpreadsheet/issues/984) + +### Changed + +- Use of `nb` rather than `no` as the locale code for Norsk Bokmål. + +### Deprecated + +- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. + +### Removed + +- Use of `nb` rather than `no` as the locale language code for Norsk Bokmål. + +### Fixed + +- Fixed error in COUPNCD() calculation for end of month [Issue #2116](https://github.com/PHPOffice/PhpSpreadsheet/issues/2116) - [PR #2119](https://github.com/PHPOffice/PhpSpreadsheet/pull/2119) +- Resolve default values when a null argument is passed for HLOOKUP(), VLOOKUP() and ADDRESS() functions [Issue #2120](https://github.com/PHPOffice/PhpSpreadsheet/issues/2120) - [PR #2121](https://github.com/PHPOffice/PhpSpreadsheet/pull/2121) +- Fixed incorrect R1C1 to A1 subtraction formula conversion (`R[-2]C-R[2]C`) [Issue #2076](https://github.com/PHPOffice/PhpSpreadsheet/pull/2076) [PR #2086](https://github.com/PHPOffice/PhpSpreadsheet/pull/2086) +- Correctly handle absolute A1 references when converting to R1C1 format [PR #2060](https://github.com/PHPOffice/PhpSpreadsheet/pull/2060) +- Correct default fill style for conditional without a pattern defined [Issue #2035](https://github.com/PHPOffice/PhpSpreadsheet/issues/2035) [PR #2050](https://github.com/PHPOffice/PhpSpreadsheet/pull/2050) +- Fixed issue where array key check for existince before accessing arrays in Xlsx.php [PR #1970](https://github.com/PHPOffice/PhpSpreadsheet/pull/1970) +- Fixed issue with quoted strings in number format mask rendered with toFormattedString() [Issue 1972#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) [PR #1978](https://github.com/PHPOffice/PhpSpreadsheet/pull/1978) +- Fixed issue with percentage formats in number format mask rendered with toFormattedString() [Issue 1929#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #1928](https://github.com/PHPOffice/PhpSpreadsheet/pull/1928) +- Fixed issue with _ spacing character in number format mask corrupting output from toFormattedString() [Issue 1924#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1924) [PR #1927](https://github.com/PHPOffice/PhpSpreadsheet/pull/1927) +- Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save +- Fixed issue with Xlsx@listWorksheetInfo not returning any data +- Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) +- Fix for [Issue #1916](https://github.com/PHPOffice/PhpSpreadsheet/issues/1916) - Invalid signature check for XML files +- Fix change in `Font::setSize()` behavior for PHP8 [PR #2100](https://github.com/PHPOffice/PhpSpreadsheet/pull/2100) + +## 1.17.1 - 2021-03-01 + +### Added + +- Implementation of the Excel `AVERAGEIFS()` functions as part of a restructuring of Database functions and Conditional Statistical functions. +- Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875) +- Support for booleans, and for wildcard text search in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876) +- Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754) +- Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796) +- Basic implementation of the PERMUTATIONA() Statistical Function + +### Changed + +- Formula functions that previously called PHP functions directly are now processed through the Excel Functions classes; resolving issues with PHP8 stricter typing. [#1789](https://github.com/PHPOffice/PhpSpreadsheet/issues/1789) + + The following MathTrig functions are affected: + `ABS()`, `ACOS()`, `ACOSH()`, `ASIN()`, `ASINH()`, `ATAN()`, `ATANH()`, + `COS()`, `COSH()`, `DEGREES()` (rad2deg), `EXP()`, `LN()` (log), `LOG10()`, + `RADIANS()` (deg2rad), `SIN()`, `SINH()`, `SQRT()`, `TAN()`, `TANH()`. + + One TextData function is also affected: `REPT()` (str_repeat). +- `formatAsDate` correctly matches language metadata, reverting c55272e +- Formulae that previously crashed on sub function call returning excel error value now return said value. + The following functions are affected `CUMPRINC()`, `CUMIPMT()`, `AMORLINC()`, + `AMORDEGRC()`. +- Adapt some function error return value to match excel's error. + The following functions are affected `PPMT()`, `IPMT()`. + +### Deprecated + +- Calling many of the Excel formula functions directly rather than through the Calculation Engine. + + The logic for these Functions is now being moved out of the categorised `Database`, `DateTime`, `Engineering`, `Financial`, `Logical`, `LookupRef`, `MathTrig`, `Statistical`, `TextData` and `Web` classes into small, dedicated classes for individual functions or related groups of functions. + + This makes the logic in these classes easier to maintain; and will reduce the memory footprint required to execute formulae when calling these functions. + +### Removed + +- Nothing. + +### Fixed + +- Avoid Duplicate Titles When Reading Multiple HTML Files.[Issue #1823](https://github.com/PHPOffice/PhpSpreadsheet/issues/1823) [PR #1829](https://github.com/PHPOffice/PhpSpreadsheet/pull/1829) +- Fixed issue with Worksheet's `getCell()` method when trying to get a cell by defined name. [#1858](https://github.com/PHPOffice/PhpSpreadsheet/issues/1858) +- Fix possible endless loop in NumberFormat Masks [#1792](https://github.com/PHPOffice/PhpSpreadsheet/issues/1792) +- Fix problem resulting from literal dot inside quotes in number format masks [PR #1830](https://github.com/PHPOffice/PhpSpreadsheet/pull/1830) +- Resolve Google Sheets Xlsx charts issue. Google Sheets uses oneCellAnchor positioning and does not include *Cache values in the exported Xlsx [PR #1761](https://github.com/PHPOffice/PhpSpreadsheet/pull/1761) +- Fix for Xlsx Chart axis titles mapping to correct X or Y axis label when only one is present [PR #1760](https://github.com/PHPOffice/PhpSpreadsheet/pull/1760) +- Fix For Null Exception on ODS Read of Page Settings. [#1772](https://github.com/PHPOffice/PhpSpreadsheet/issues/1772) +- Fix Xlsx reader overriding manually set number format with builtin number format [PR #1805](https://github.com/PHPOffice/PhpSpreadsheet/pull/1805) +- Fix Xlsx reader cell alignment [PR #1710](https://github.com/PHPOffice/PhpSpreadsheet/pull/1710) +- Fix for not yet implemented data-types in Open Document writer [Issue #1674](https://github.com/PHPOffice/PhpSpreadsheet/issues/1674) +- Fix XLSX reader when having a corrupt numeric cell data type [PR #1664](https://github.com/phpoffice/phpspreadsheet/pull/1664) +- Fix on `CUMPRINC()`, `CUMIPMT()`, `AMORLINC()`, `AMORDEGRC()` usage. When those functions called one of `YEARFRAC()`, `PPMT()`, `IPMT()` and they would get back an error value (represented as a string), trying to use numeral operands (`+`, `/`, `-`, `*`) on said return value and a number (`float or `int`) would fail. + +## 1.16.0 - 2020-12-31 + +### Added + +- CSV Reader - Best Guess for Encoding, and Handle Null-string Escape [#1647](https://github.com/PHPOffice/PhpSpreadsheet/issues/1647) + +### Changed + +- Updated the CONVERT() function to support all current MS Excel categories and Units of Measure. + +### Deprecated + +- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. + +### Removed + +- Nothing. + +### Fixed + +- Fixed issue with absolute path in worksheets' Target [PR #1769](https://github.com/PHPOffice/PhpSpreadsheet/pull/1769) +- Fix for Xls Reader when SST has a bad length [#1592](https://github.com/PHPOffice/PhpSpreadsheet/issues/1592) +- Resolve Xlsx loader issue whe hyperlinks don't have a destination +- Resolve issues when printer settings resources IDs clash with drawing IDs +- Resolve issue with SLK long filenames [#1612](https://github.com/PHPOffice/PhpSpreadsheet/issues/1612) +- ROUNDUP and ROUNDDOWN return incorrect results for values of 0 [#1627](https://github.com/phpoffice/phpspreadsheet/pull/1627) +- Apply Column and Row Styles to Existing Cells [#1712](https://github.com/PHPOffice/PhpSpreadsheet/issues/1712) [PR #1721](https://github.com/PHPOffice/PhpSpreadsheet/pull/1721) +- Resolve issues with defined names where worksheet doesn't exist (#1686)[https://github.com/PHPOffice/PhpSpreadsheet/issues/1686] and [#1723](https://github.com/PHPOffice/PhpSpreadsheet/issues/1723) - [PR #1742](https://github.com/PHPOffice/PhpSpreadsheet/pull/1742) +- Fix for issue [#1735](https://github.com/PHPOffice/PhpSpreadsheet/issues/1735) Incorrect activeSheetIndex after RemoveSheetByIndex - [PR #1743](https://github.com/PHPOffice/PhpSpreadsheet/pull/1743) +- Ensure that the list of shared formulae is maintained when an xlsx file is chunked with readFilter[Issue #169](https://github.com/PHPOffice/PhpSpreadsheet/issues/1669). +- Fix for notice during accessing "cached magnification factor" offset [#1354](https://github.com/PHPOffice/PhpSpreadsheet/pull/1354) +- Fix compatibility with ext-gd on php 8 + +### Security Fix (CVE-2020-7776) + +- Prevent XSS through cell comments in the HTML Writer. + +## 1.15.0 - 2020-10-11 + +### Added + +- Implemented Page Order for Xlsx and Xls Readers, and provided Page Settings (Orientation, Scale, Horizontal/Vertical Centering, Page Order, Margins) support for Ods, Gnumeric and Xls Readers [#1559](https://github.com/PHPOffice/PhpSpreadsheet/pull/1559) +- Implementation of the Excel `LOGNORM.DIST()`, `NORM.S.DIST()`, `GAMMA()` and `GAUSS()` functions. [#1588](https://github.com/PHPOffice/PhpSpreadsheet/pull/1588) +- Named formula implementation, and improved handling of Defined Names generally [#1535](https://github.com/PHPOffice/PhpSpreadsheet/pull/1535) + - Defined Names are now case-insensitive + - Distinction between named ranges and named formulae + - Correct handling of union and intersection operators in named ranges + - Correct evaluation of named range operators in calculations + - fix resolution of relative named range values in the calculation engine; previously all named range values had been treated as absolute. + - Calculation support for named formulae + - Support for nested ranges and formulae (named ranges and formulae that reference other named ranges/formulae) in calculations + - Introduction of a helper to convert address formats between R1C1 and A1 (and the reverse) + - Proper support for both named ranges and named formulae in all appropriate Readers + - **Xlsx** (Previously only simple named ranges were supported) + - **Xls** (Previously only simple named ranges were supported) + - **Gnumeric** (Previously neither named ranges nor formulae were supported) + - **Ods** (Previously neither named ranges nor formulae were supported) + - **Xml** (Previously neither named ranges nor formulae were supported) + - Proper support for named ranges and named formulae in all appropriate Writers + - **Xlsx** (Previously only simple named ranges were supported) + - **Xls** (Previously neither named ranges nor formulae were supported) - Still not supported, but some parser issues resolved that previously failed to differentiate between a defined name and a function name + - **Ods** (Previously neither named ranges nor formulae were supported) +- Support for PHP 8.0 + +### Changed + +- Improve Coverage for ODS Reader [#1545](https://github.com/phpoffice/phpspreadsheet/pull/1545) +- Named formula implementation, and improved handling of Defined Names generally [#1535](https://github.com/PHPOffice/PhpSpreadsheet/pull/1535) +- fix resolution of relative named range values in the calculation engine; previously all named range values had been treated as absolute. +- Drop $this->spreadSheet null check from Xlsx Writer [#1646](https://github.com/phpoffice/phpspreadsheet/pull/1646) +- Improving Coverage for Excel2003 XML Reader [#1557](https://github.com/phpoffice/phpspreadsheet/pull/1557) + +### Deprecated + +- **IMPORTANT NOTE:** This Introduces a **BC break** in the handling of named ranges. Previously, a named range cell reference of `B2` would be treated identically to a named range cell reference of `$B2` or `B$2` or `$B$2` because the calculation engine treated then all as absolute references. These changes "fix" that, so the calculation engine now handles relative references in named ranges correctly. + This change that resolves previously incorrect behaviour in the calculation may affect users who have dynamically defined named ranges using relative references when they should have used absolute references. + +### Removed + +- Nothing. + +### Fixed + +- PrintArea causes exception [#1544](https://github.com/phpoffice/phpspreadsheet/pull/1544) +- Calculation/DateTime Failure With PHP8 [#1661](https://github.com/phpoffice/phpspreadsheet/pull/1661) +- Reader/Gnumeric Failure with PHP8 [#1662](https://github.com/phpoffice/phpspreadsheet/pull/1662) +- ReverseSort bug, exposed but not caused by PHP8 [#1660](https://github.com/phpoffice/phpspreadsheet/pull/1660) +- Bug setting Superscript/Subscript to false [#1567](https://github.com/phpoffice/phpspreadsheet/pull/1567) + +## 1.14.1 - 2020-07-19 + +### Added + +- nothing + +### Fixed + +- WEBSERVICE is HTTP client agnostic and must be configured via `Settings::setHttpClient()` [#1562](https://github.com/PHPOffice/PhpSpreadsheet/issues/1562) +- Borders were not complete on rowspanned columns using HTML reader [#1473](https://github.com/PHPOffice/PhpSpreadsheet/pull/1473) + +### Changed + +## 1.14.0 - 2020-06-29 + +### Added + +- Add support for IFS() logical function [#1442](https://github.com/PHPOffice/PhpSpreadsheet/pull/1442) +- Add Cell Address Helper to provide conversions between the R1C1 and A1 address formats [#1558](https://github.com/PHPOffice/PhpSpreadsheet/pull/1558) +- Add ability to edit Html/Pdf before saving [#1499](https://github.com/PHPOffice/PhpSpreadsheet/pull/1499) +- Add ability to set codepage explicitly for BIFF5 [#1018](https://github.com/PHPOffice/PhpSpreadsheet/issues/1018) +- Added support for the WEBSERVICE function [#1409](https://github.com/PHPOffice/PhpSpreadsheet/pull/1409) + +### Fixed + +- Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) +- Fix HLOOKUP on single row [#1512](https://github.com/PHPOffice/PhpSpreadsheet/pull/1512) +- Fix MATCH when comparing different numeric types [#1521](https://github.com/PHPOffice/PhpSpreadsheet/pull/1521) +- Fix exact MATCH on ranges with empty cells [#1520](https://github.com/PHPOffice/PhpSpreadsheet/pull/1520) +- Fix for Issue [#1516](https://github.com/PHPOffice/PhpSpreadsheet/issues/1516) (Cloning worksheet makes corrupted Xlsx) [#1530](https://github.com/PHPOffice/PhpSpreadsheet/pull/1530) +- Fix For Issue [#1509](https://github.com/PHPOffice/PhpSpreadsheet/issues/1509) (Can not set empty enclosure for CSV) [#1518](https://github.com/PHPOffice/PhpSpreadsheet/pull/1518) +- Fix for Issue [#1505](https://github.com/PHPOffice/PhpSpreadsheet/issues/1505) (TypeError : Argument 4 passed to PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet::writeAttributeIf() must be of the type string) [#1525](https://github.com/PHPOffice/PhpSpreadsheet/pull/1525) +- Fix for Issue [#1495](https://github.com/PHPOffice/PhpSpreadsheet/issues/1495) (Sheet index being changed when multiple sheets are used in formula) [#1500]((https://github.com/PHPOffice/PhpSpreadsheet/pull/1500)) +- Fix for Issue [#1533](https://github.com/PHPOffice/PhpSpreadsheet/issues/1533) (A reference to a cell containing a string starting with "#" leads to errors in the generated xlsx.) [#1534](https://github.com/PHPOffice/PhpSpreadsheet/pull/1534) +- Xls Writer - Correct Timestamp Bug [#1493](https://github.com/PHPOffice/PhpSpreadsheet/pull/1493) +- Don't ouput row and columns without any cells in HTML writer [#1235](https://github.com/PHPOffice/PhpSpreadsheet/issues/1235) + +## 1.13.0 - 2020-05-31 + +### Added + +- Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292) +- Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468) +- Support protection of worksheet by a specific hash algorithm [#1485](https://github.com/PHPOffice/PhpSpreadsheet/pull/1485) + +### Fixed + +- Fix Chart samples by updating chart parameter from 0 to DataSeries::EMPTY_AS_GAP [#1448](https://github.com/PHPOffice/PhpSpreadsheet/pull/1448) +- Fix return type in docblock for the Cells::get() [#1398](https://github.com/PHPOffice/PhpSpreadsheet/pull/1398) +- Fix RATE, PRICE, XIRR, and XNPV Functions [#1456](https://github.com/PHPOffice/PhpSpreadsheet/pull/1456) +- Save Excel 2010+ functions properly in XLSX [#1461](https://github.com/PHPOffice/PhpSpreadsheet/pull/1461) +- Several improvements in HTML writer [#1464](https://github.com/PHPOffice/PhpSpreadsheet/pull/1464) +- Fix incorrect behaviour when saving XLSX file with drawings [#1462](https://github.com/PHPOffice/PhpSpreadsheet/pull/1462), +- Fix Crash while trying setting a cell the value "123456\n" [#1476](https://github.com/PHPOffice/PhpSpreadsheet/pull/1481) +- Improved DATEDIF() function and reduced errors for Y and YM units [#1466](https://github.com/PHPOffice/PhpSpreadsheet/pull/1466) +- Stricter typing for mergeCells [#1494](https://github.com/PHPOffice/PhpSpreadsheet/pull/1494) + +### Changed + +- Drop support for PHP 7.1, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support +- Drop partial migration tool in favor of complete migration via RectorPHP [#1445](https://github.com/PHPOffice/PhpSpreadsheet/issues/1445) +- Limit composer package to `src/` [#1424](https://github.com/PHPOffice/PhpSpreadsheet/pull/1424) + +## 1.12.0 - 2020-04-27 + +### Added + +- Improved the ARABIC function to also handle short-hand roman numerals +- Added support for the FLOOR.MATH and FLOOR.PRECISE functions [#1351](https://github.com/PHPOffice/PhpSpreadsheet/pull/1351) + +### Fixed + +- Fix ROUNDUP and ROUNDDOWN for floating-point rounding error [#1404](https://github.com/PHPOffice/PhpSpreadsheet/pull/1404) +- Fix ROUNDUP and ROUNDDOWN for negative number [#1417](https://github.com/PHPOffice/PhpSpreadsheet/pull/1417) +- Fix loading styles from vmlDrawings when containing whitespace [#1347](https://github.com/PHPOffice/PhpSpreadsheet/issues/1347) +- Fix incorrect behavior when removing last row [#1365](https://github.com/PHPOffice/PhpSpreadsheet/pull/1365) +- MATCH with a static array should return the position of the found value based on the values submitted [#1332](https://github.com/PHPOffice/PhpSpreadsheet/pull/1332) +- Fix Xlsx Reader's handling of undefined fill color [#1353](https://github.com/PHPOffice/PhpSpreadsheet/pull/1353) + +## 1.11.0 - 2020-03-02 + +### Added + +- Added support for the BASE function +- Added support for the ARABIC function +- Conditionals - Extend Support for (NOT)CONTAINSBLANKS [#1278](https://github.com/PHPOffice/PhpSpreadsheet/pull/1278) + +### Fixed + +- Handle Error in Formula Processing Better for Xls [#1267](https://github.com/PHPOffice/PhpSpreadsheet/pull/1267) +- Handle ConditionalStyle NumberFormat When Reading Xlsx File [#1296](https://github.com/PHPOffice/PhpSpreadsheet/pull/1296) +- Fix Xlsx Writer's handling of decimal commas [#1282](https://github.com/PHPOffice/PhpSpreadsheet/pull/1282) +- Fix for issue by removing test code mistakenly left in [#1328](https://github.com/PHPOffice/PhpSpreadsheet/pull/1328) +- Fix for Xls writer wrong selected cells and active sheet [#1256](https://github.com/PHPOffice/PhpSpreadsheet/pull/1256) +- Fix active cell when freeze pane is used [#1323](https://github.com/PHPOffice/PhpSpreadsheet/pull/1323) +- Fix XLSX file loading with autofilter containing '$' [#1326](https://github.com/PHPOffice/PhpSpreadsheet/pull/1326) +- PHPDoc - Use `@return $this` for fluent methods [#1362](https://github.com/PHPOffice/PhpSpreadsheet/pull/1362) + +## 1.10.1 - 2019-12-02 + +### Changed + +- PHP 7.4 compatibility + +### Fixed + +- FLOOR() function accept negative number and negative significance [#1245](https://github.com/PHPOffice/PhpSpreadsheet/pull/1245) +- Correct column style even when using rowspan [#1249](https://github.com/PHPOffice/PhpSpreadsheet/pull/1249) +- Do not confuse defined names and cell refs [#1263](https://github.com/PHPOffice/PhpSpreadsheet/pull/1263) +- XLSX reader/writer keep decimal for floats with a zero decimal part [#1262](https://github.com/PHPOffice/PhpSpreadsheet/pull/1262) +- ODS writer prevent invalid numeric value if locale decimal separator is comma [#1268](https://github.com/PHPOffice/PhpSpreadsheet/pull/1268) +- Xlsx writer actually writes plotVisOnly and dispBlanksAs from chart properties [#1266](https://github.com/PHPOffice/PhpSpreadsheet/pull/1266) + +## 1.10.0 - 2019-11-18 + +### Changed + +- Change license from LGPL 2.1 to MIT [#140](https://github.com/PHPOffice/PhpSpreadsheet/issues/140) + +### Added + +- Implementation of IFNA() logical function +- Support "showZeros" worksheet option to change how Excel shows and handles "null" values returned from a calculation +- Allow HTML Reader to accept HTML as a string into an existing spreadsheet [#1212](https://github.com/PHPOffice/PhpSpreadsheet/pull/1212) + +### Fixed + +- IF implementation properly handles the value `#N/A` [#1165](https://github.com/PHPOffice/PhpSpreadsheet/pull/1165) +- Formula Parser: Wrong line count for stuff like "MyOtherSheet!A:D" [#1215](https://github.com/PHPOffice/PhpSpreadsheet/issues/1215) +- Call garbage collector after removing a column to prevent stale cached values +- Trying to remove a column that doesn't exist deletes the latest column +- Keep big integer as integer instead of lossely casting to float [#874](https://github.com/PHPOffice/PhpSpreadsheet/pull/874) +- Fix branch pruning handling of non boolean conditions [#1167](https://github.com/PHPOffice/PhpSpreadsheet/pull/1167) +- Fix ODS Reader when no DC namespace are defined [#1182](https://github.com/PHPOffice/PhpSpreadsheet/pull/1182) +- Fixed Functions->ifCondition for allowing <> and empty condition [#1206](https://github.com/PHPOffice/PhpSpreadsheet/pull/1206) +- Validate XIRR inputs and return correct error values [#1120](https://github.com/PHPOffice/PhpSpreadsheet/issues/1120) +- Allow to read xlsx files with exotic workbook names like "workbook2.xml" [#1183](https://github.com/PHPOffice/PhpSpreadsheet/pull/1183) + +## 1.9.0 - 2019-08-17 + +### Changed + +- Drop support for PHP 5.6 and 7.0, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support + +### Added + +- When <br> appears in a table cell, set the cell to wrap [#1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [#1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070) +- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF [#1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056) +- HLookup needs an ordered list even if range_lookup is set to false [#1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [#1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076) +- Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844) +- MATCH function supports `*?~` Excel functionality, when match_type=0 [#1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) +- Allow HTML Reader to accept HTML as a string [#1136](https://github.com/PHPOffice/PhpSpreadsheet/pull/1136) + +### Fixed + +- Fix to AVERAGEIF() function when called with a third argument +- Eliminate duplicate fill none style entries [#1066](https://github.com/PHPOffice/PhpSpreadsheet/issues/1066) +- Fix number format masks containing literal (non-decimal point) dots [#1079](https://github.com/PHPOffice/PhpSpreadsheet/issues/1079) +- Fix number format masks containing named colours that were being misinterpreted as date formats; and add support for masks that fully replace the value with a full text string [#1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009) +- Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [#1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046) +- COUPNUM should not return zero when settlement is in the last period [#1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [#1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021) +- Fix handling of named ranges referencing sheets with spaces or "!" in their title +- Cover `getSheetByName()` with tests for name with quote and spaces [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) +- Best effort to support invalid colspan values in HTML reader - [#878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) +- Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868) +- MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) [#1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) +- Fix `getCalculatedValue()` error with more than two INDIRECT [#1115](https://github.com/PHPOffice/PhpSpreadsheet/pull/1115) +- Writer\Html did not hide columns [#985](https://github.com/PHPOffice/PhpSpreadsheet/pull/985) + +## 1.8.2 - 2019-07-08 + +### Fixed + +- Uncaught error when opening ods file and properties aren't defined [#1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047) +- Xlsx Reader Cell datavalidations bug [#1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052) + +## 1.8.1 - 2019-07-02 + +### Fixed + +- Allow nullable theme for Xlsx Style Reader class [#1043](https://github.com/PHPOffice/PhpSpreadsheet/issues/1043) + +## 1.8.0 - 2019-07-01 + +### Security Fix (CVE-2019-12331) + +- Detect double-encoded xml in the Security scanner, and reject as suspicious. +- This change also broadens the scope of the `libxml_disable_entity_loader` setting when reading XML-based formats, so that it is enabled while the xml is being parsed and not simply while it is loaded. + On some versions of PHP, this can cause problems because it is not thread-safe, and can affect other PHP scripts running on the same server. This flag is set to true when instantiating a loader, and back to its original setting when the Reader is no longer in scope, or manually unset. +- Provide a check to identify whether libxml_disable_entity_loader is thread-safe or not. + + `XmlScanner::threadSafeLibxmlDisableEntityLoaderAvailability()` +- Provide an option to disable the libxml_disable_entity_loader call through settings. This is not recommended as it reduces the security of the XML-based readers, and should only be used if you understand the consequences and have no other choice. + +### Added + +- Added support for the SWITCH function [#963](https://github.com/PHPOffice/PhpSpreadsheet/issues/963) and [#983](https://github.com/PHPOffice/PhpSpreadsheet/pull/983) +- Add accounting number format style [#974](https://github.com/PHPOffice/PhpSpreadsheet/pull/974) + +### Fixed + +- Whitelist `tsv` extension when opening CSV files [#429](https://github.com/PHPOffice/PhpSpreadsheet/issues/429) +- Fix a SUMIF warning with some versions of PHP when having different length of arrays provided as input [#873](https://github.com/PHPOffice/PhpSpreadsheet/pull/873) +- Fix incorrectly handled backslash-escaped space characters in number format + +## 1.7.0 - 2019-05-26 + +- Added support for inline styles in Html reader (borders, alignment, width, height) +- QuotedText cells no longer treated as formulae if the content begins with a `=` +- Clean handling for DDE in formulae + +### Fixed + +- Fix handling for escaped enclosures and new lines in CSV Separator Inference +- Fix MATCH an error was appearing when comparing strings against 0 (always true) +- Fix wrong calculation of highest column with specified row [#700](https://github.com/PHPOffice/PhpSpreadsheet/issues/700) +- Fix VLOOKUP +- Fix return type hint + +## 1.6.0 - 2019-01-02 + +### Added + +- Refactored Matrix Functions to use external Matrix library +- Possibility to specify custom colors of values for pie and donut charts [#768](https://github.com/PHPOffice/PhpSpreadsheet/pull/768) + +### Fixed + +- Improve XLSX parsing speed if no readFilter is applied [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772) +- Fix column names if read filter calls in XLSX reader skip columns [#777](https://github.com/PHPOffice/PhpSpreadsheet/pull/777) +- XLSX reader can now ignore blank cells, using the setReadEmptyCells(false) method. [#810](https://github.com/PHPOffice/PhpSpreadsheet/issues/810) +- Fix LOOKUP function which was breaking on edge cases [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796) +- Fix VLOOKUP with exact matches [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809) +- Support COUNTIFS multiple arguments [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830) +- Change `libxml_disable_entity_loader()` as shortly as possible [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819) +- Improved memory usage and performance when loading large spreadsheets [#822](https://github.com/PHPOffice/PhpSpreadsheet/pull/822) +- Improved performance when loading large spreadsheets [#825](https://github.com/PHPOffice/PhpSpreadsheet/pull/825) +- Improved performance when loading large spreadsheets [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824) +- Fix color from CSS when reading from HTML [#831](https://github.com/PHPOffice/PhpSpreadsheet/pull/831) +- Fix infinite loop when reading invalid ODS files [#832](https://github.com/PHPOffice/PhpSpreadsheet/pull/832) +- Fix time format for duration is incorrect [#666](https://github.com/PHPOffice/PhpSpreadsheet/pull/666) +- Fix iconv unsupported `//IGNORE//TRANSLIT` on IBM i [#791](https://github.com/PHPOffice/PhpSpreadsheet/issues/791) + +### Changed + +- `master` is the new default branch, `develop` does not exist anymore + +## 1.5.2 - 2018-11-25 + +### Security + +- Improvements to the design of the XML Security Scanner [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771) + +## 1.5.1 - 2018-11-20 + +### Security + +- Fix and improve XXE security scanning for XML-based and HTML Readers [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771) + +### Added + +- Support page margin in mPDF [#750](https://github.com/PHPOffice/PhpSpreadsheet/issues/750) + +### Fixed + +- Support numeric condition in SUMIF, SUMIFS, AVERAGEIF, COUNTIF, MAXIF and MINIF [#683](https://github.com/PHPOffice/PhpSpreadsheet/issues/683) +- SUMIFS containing multiple conditions [#704](https://github.com/PHPOffice/PhpSpreadsheet/issues/704) +- Csv reader avoid notice when the file is empty [#743](https://github.com/PHPOffice/PhpSpreadsheet/pull/743) +- Fix print area parser for XLSX reader [#734](https://github.com/PHPOffice/PhpSpreadsheet/pull/734) +- Support overriding `DefaultValueBinder::dataTypeForValue()` without overriding `DefaultValueBinder::bindValue()` [#735](https://github.com/PHPOffice/PhpSpreadsheet/pull/735) +- Mpdf export can exceed pcre.backtrack_limit [#637](https://github.com/PHPOffice/PhpSpreadsheet/issues/637) +- Fix index overflow on data values array [#748](https://github.com/PHPOffice/PhpSpreadsheet/pull/748) + +## 1.5.0 - 2018-10-21 + +### Added + +- PHP 7.3 support +- Add the DAYS() function [#594](https://github.com/PHPOffice/PhpSpreadsheet/pull/594) + +### Fixed + +- Sheet title can contain exclamation mark [#325](https://github.com/PHPOffice/PhpSpreadsheet/issues/325) +- Xls file cause the exception during open by Xls reader [#402](https://github.com/PHPOffice/PhpSpreadsheet/issues/402) +- Skip non numeric value in SUMIF [#618](https://github.com/PHPOffice/PhpSpreadsheet/pull/618) +- OFFSET should allow omitted height and width [#561](https://github.com/PHPOffice/PhpSpreadsheet/issues/561) +- Correctly determine delimiter when CSV contains line breaks inside enclosures [#716](https://github.com/PHPOffice/PhpSpreadsheet/issues/716) + +## 1.4.1 - 2018-09-30 + +### Fixed + +- Remove locale from formatting string [#644](https://github.com/PHPOffice/PhpSpreadsheet/pull/644) +- Allow iterators to go out of bounds with prev [#587](https://github.com/PHPOffice/PhpSpreadsheet/issues/587) +- Fix warning when reading xlsx without styles [#631](https://github.com/PHPOffice/PhpSpreadsheet/pull/631) +- Fix broken sample links on windows due to $baseDir having backslash [#653](https://github.com/PHPOffice/PhpSpreadsheet/pull/653) + +## 1.4.0 - 2018-08-06 + +### Added + +- Add excel function EXACT(value1, value2) support [#595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595) +- Support workbook view attributes for Xlsx format [#523](https://github.com/PHPOffice/PhpSpreadsheet/issues/523) +- Read and write hyperlink for drawing image [#490](https://github.com/PHPOffice/PhpSpreadsheet/pull/490) +- Added calculation engine support for the new bitwise functions that were added in MS Excel 2013 + - BITAND() Returns a Bitwise 'And' of two numbers + - BITOR() Returns a Bitwise 'Or' of two number + - BITXOR() Returns a Bitwise 'Exclusive Or' of two numbers + - BITLSHIFT() Returns a number shifted left by a specified number of bits + - BITRSHIFT() Returns a number shifted right by a specified number of bits +- Added calculation engine support for other new functions that were added in MS Excel 2013 and MS Excel 2016 + - Text Functions + - CONCAT() Synonym for CONCATENATE() + - NUMBERVALUE() Converts text to a number, in a locale-independent way + - UNICHAR() Synonym for CHAR() in PHPSpreadsheet, which has always used UTF-8 internally + - UNIORD() Synonym for ORD() in PHPSpreadsheet, which has always used UTF-8 internally + - TEXTJOIN() Joins together two or more text strings, separated by a delimiter + - Logical Functions + - XOR() Returns a logical Exclusive Or of all arguments + - Date/Time Functions + - ISOWEEKNUM() Returns the ISO 8601 week number of the year for a given date + - Lookup and Reference Functions + - FORMULATEXT() Returns a formula as a string + - Financial Functions + - PDURATION() Calculates the number of periods required for an investment to reach a specified value + - RRI() Calculates the interest rate required for an investment to grow to a specified future value + - Engineering Functions + - ERF.PRECISE() Returns the error function integrated between 0 and a supplied limit + - ERFC.PRECISE() Synonym for ERFC + - Math and Trig Functions + - SEC() Returns the secant of an angle + - SECH() Returns the hyperbolic secant of an angle + - CSC() Returns the cosecant of an angle + - CSCH() Returns the hyperbolic cosecant of an angle + - COT() Returns the cotangent of an angle + - COTH() Returns the hyperbolic cotangent of an angle + - ACOT() Returns the cotangent of an angle + - ACOTH() Returns the hyperbolic cotangent of an angle +- Refactored Complex Engineering Functions to use external complex number library +- Added calculation engine support for the new complex number functions that were added in MS Excel 2013 + - IMCOSH() Returns the hyperbolic cosine of a complex number + - IMCOT() Returns the cotangent of a complex number + - IMCSC() Returns the cosecant of a complex number + - IMCSCH() Returns the hyperbolic cosecant of a complex number + - IMSEC() Returns the secant of a complex number + - IMSECH() Returns the hyperbolic secant of a complex number + - IMSINH() Returns the hyperbolic sine of a complex number + - IMTAN() Returns the tangent of a complex number + +### Fixed + +- Fix ISFORMULA() function to work with a cell reference to another worksheet +- Xlsx reader crashed when reading a file with workbook protection [#553](https://github.com/PHPOffice/PhpSpreadsheet/pull/553) +- Cell formats with escaped spaces were causing incorrect date formatting [#557](https://github.com/PHPOffice/PhpSpreadsheet/issues/557) +- Could not open CSV file containing HTML fragment [#564](https://github.com/PHPOffice/PhpSpreadsheet/issues/564) +- Exclude the vendor folder in migration [#481](https://github.com/PHPOffice/PhpSpreadsheet/issues/481) +- Chained operations on cell ranges involving borders operated on last cell only [#428](https://github.com/PHPOffice/PhpSpreadsheet/issues/428) +- Avoid memory exhaustion when cloning worksheet with a drawing [#437](https://github.com/PHPOffice/PhpSpreadsheet/issues/437) +- Migration tool keep variables containing $PHPExcel untouched [#598](https://github.com/PHPOffice/PhpSpreadsheet/issues/598) +- Rowspans/colspans were incorrect when adding worksheet using loadIntoExisting [#619](https://github.com/PHPOffice/PhpSpreadsheet/issues/619) + +## 1.3.1 - 2018-06-12 + +### Fixed + +- Ranges across Z and AA columns incorrectly threw an exception [#545](https://github.com/PHPOffice/PhpSpreadsheet/issues/545) + +## 1.3.0 - 2018-06-10 + +### Added + +- Support to read Xlsm templates with form elements, macros, printer settings, protected elements and back compatibility drawing, and save result without losing important elements of document [#435](https://github.com/PHPOffice/PhpSpreadsheet/issues/435) +- Expose sheet title maximum length as `Worksheet::SHEET_TITLE_MAXIMUM_LENGTH` [#482](https://github.com/PHPOffice/PhpSpreadsheet/issues/482) +- Allow escape character to be set in CSV reader [#492](https://github.com/PHPOffice/PhpSpreadsheet/issues/492) + +### Fixed + +- Subtotal 9 in a group that has other subtotals 9 exclude the totals of the other subtotals in the range [#332](https://github.com/PHPOffice/PhpSpreadsheet/issues/332) +- `Helper\Html` support UTF-8 HTML input [#444](https://github.com/PHPOffice/PhpSpreadsheet/issues/444) +- Xlsx loaded an extra empty comment for each real comment [#375](https://github.com/PHPOffice/PhpSpreadsheet/issues/375) +- Xlsx reader do not read rows and columns filtered out in readFilter at all [#370](https://github.com/PHPOffice/PhpSpreadsheet/issues/370) +- Make newer Excel versions properly recalculate formulas on document open [#456](https://github.com/PHPOffice/PhpSpreadsheet/issues/456) +- `Coordinate::extractAllCellReferencesInRange()` throws an exception for an invalid range [#519](https://github.com/PHPOffice/PhpSpreadsheet/issues/519) +- Fixed parsing of conditionals in COUNTIF functions [#526](https://github.com/PHPOffice/PhpSpreadsheet/issues/526) +- Corruption errors for saved Xlsx docs with frozen panes [#532](https://github.com/PHPOffice/PhpSpreadsheet/issues/532) + +## 1.2.1 - 2018-04-10 + +### Fixed + +- Plain text and richtext mixed in same cell can be read [#442](https://github.com/PHPOffice/PhpSpreadsheet/issues/442) + +## 1.2.0 - 2018-03-04 + +### Added + +- HTML writer creates a generator meta tag [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312) +- Support invalid zoom value in XLSX format [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350) +- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390) + +### Fixed + +- Avoid potentially unsupported PSR-16 cache keys [#354](https://github.com/PHPOffice/PhpSpreadsheet/issues/354) +- Check for MIME type to know if CSV reader can read a file [#167](https://github.com/PHPOffice/PhpSpreadsheet/issues/167) +- Use proper € symbol for currency format [#379](https://github.com/PHPOffice/PhpSpreadsheet/pull/379) +- Read printing area correctly when skipping some sheets [#371](https://github.com/PHPOffice/PhpSpreadsheet/issues/371) +- Avoid incorrectly overwriting calculated value type [#394](https://github.com/PHPOffice/PhpSpreadsheet/issues/394) +- Select correct cell when calling freezePane [#389](https://github.com/PHPOffice/PhpSpreadsheet/issues/389) +- `setStrikethrough()` did not set the font [#403](https://github.com/PHPOffice/PhpSpreadsheet/issues/403) + +## 1.1.0 - 2018-01-28 + +### Added + +- Support for PHP 7.2 +- Support cell comments in HTML writer and reader [#308](https://github.com/PHPOffice/PhpSpreadsheet/issues/308) +- Option to stop at a conditional styling, if it matches (only XLSX format) [#292](https://github.com/PHPOffice/PhpSpreadsheet/pull/292) +- Support for line width for data series when rendering Xlsx [#329](https://github.com/PHPOffice/PhpSpreadsheet/pull/329) + +### Fixed + +- Better auto-detection of CSV separators [#305](https://github.com/PHPOffice/PhpSpreadsheet/issues/305) +- Support for shape style ending with `;` [#304](https://github.com/PHPOffice/PhpSpreadsheet/issues/304) +- Freeze Panes takes wrong coordinates for XLSX [#322](https://github.com/PHPOffice/PhpSpreadsheet/issues/322) +- `COLUMNS` and `ROWS` functions crashed in some cases [#336](https://github.com/PHPOffice/PhpSpreadsheet/issues/336) +- Support XML file without styles [#331](https://github.com/PHPOffice/PhpSpreadsheet/pull/331) +- Cell coordinates which are already a range cause an exception [#319](https://github.com/PHPOffice/PhpSpreadsheet/issues/319) + +## 1.0.0 - 2017-12-25 + +### Added + +- Support to write merged cells in ODS format [#287](https://github.com/PHPOffice/PhpSpreadsheet/issues/287) +- Able to set the `topLeftCell` in freeze panes [#261](https://github.com/PHPOffice/PhpSpreadsheet/pull/261) +- Support `DateTimeImmutable` as cell value +- Support migration of prefixed classes + +### Fixed + +- Can read very small HTML files [#194](https://github.com/PHPOffice/PhpSpreadsheet/issues/194) +- Written DataValidation was corrupted [#290](https://github.com/PHPOffice/PhpSpreadsheet/issues/290) +- Date format compatible with both LibreOffice and Excel [#298](https://github.com/PHPOffice/PhpSpreadsheet/issues/298) + +### BREAKING CHANGE + +- Constant `TYPE_DOUGHTNUTCHART` is now `TYPE_DOUGHNUTCHART`. + +## 1.0.0-beta2 - 2017-11-26 + +### Added + +- Support for chart fill color - @CrazyBite [#158](https://github.com/PHPOffice/PhpSpreadsheet/pull/158) +- Support for read Hyperlink for xml - @GreatHumorist [#223](https://github.com/PHPOffice/PhpSpreadsheet/pull/223) +- Support for cell value validation according to data validation rules - @SailorMax [#257](https://github.com/PHPOffice/PhpSpreadsheet/pull/257) +- Support for custom implementation, or configuration, of PDF libraries - @SailorMax [#266](https://github.com/PHPOffice/PhpSpreadsheet/pull/266) + +### Changed + +- Merge data-validations to reduce written worksheet size - @billblume [#131](https://github.com/PHPOffice/PhpSpreadSheet/issues/131) +- Throws exception if a XML file is invalid - @GreatHumorist [#222](https://github.com/PHPOffice/PhpSpreadsheet/pull/222) +- Upgrade to mPDF 7.0+ [#144](https://github.com/PHPOffice/PhpSpreadsheet/issues/144) + +### Fixed + +- Control characters in cell values are automatically escaped [#212](https://github.com/PHPOffice/PhpSpreadsheet/issues/212) +- Prevent color changing when copy/pasting xls files written by PhpSpreadsheet to another file - @al-lala [#218](https://github.com/PHPOffice/PhpSpreadsheet/issues/218) +- Add cell reference automatic when there is no cell reference('r' attribute) in Xlsx file. - @GreatHumorist [#225](https://github.com/PHPOffice/PhpSpreadsheet/pull/225) Refer to [#201](https://github.com/PHPOffice/PhpSpreadsheet/issues/201) +- `Reader\Xlsx::getFromZipArchive()` function return false if the zip entry could not be located. - @anton-harvey [#268](https://github.com/PHPOffice/PhpSpreadsheet/pull/268) + +### BREAKING CHANGE + +- Extracted coordinate method to dedicate class [migration guide](./docs/topics/migration-from-PHPExcel.md). +- Column indexes are based on 1, see the [migration guide](./docs/topics/migration-from-PHPExcel.md). +- Standardization of array keys used for style, see the [migration guide](./docs/topics/migration-from-PHPExcel.md). +- Easier usage of PDF writers, and other custom readers and writers, see the [migration guide](./docs/topics/migration-from-PHPExcel.md). +- Easier usage of chart renderers, see the [migration guide](./docs/topics/migration-from-PHPExcel.md). +- Rename a few more classes to keep them in their related namespaces: + - `CalcEngine` => `Calculation\Engine` + - `PhpSpreadsheet\Calculation` => `PhpSpreadsheet\Calculation\Calculation` + - `PhpSpreadsheet\Cell` => `PhpSpreadsheet\Cell\Cell` + - `PhpSpreadsheet\Chart` => `PhpSpreadsheet\Chart\Chart` + - `PhpSpreadsheet\RichText` => `PhpSpreadsheet\RichText\RichText` + - `PhpSpreadsheet\Style` => `PhpSpreadsheet\Style\Style` + - `PhpSpreadsheet\Worksheet` => `PhpSpreadsheet\Worksheet\Worksheet` + +## 1.0.0-beta - 2017-08-17 + +### Added + +- Initial implementation of SUMIFS() function +- Additional codepages +- MemoryDrawing not working in HTML writer [#808](https://github.com/PHPOffice/PHPExcel/issues/808) +- CSV Reader can auto-detect the separator used in file [#141](https://github.com/PHPOffice/PhpSpreadsheet/pull/141) +- HTML Reader supports some basic inline styles [#180](https://github.com/PHPOffice/PhpSpreadsheet/pull/180) + +### Changed + +- Start following [SemVer](https://semver.org) properly. + +### Fixed + +- Fix to getCell() method when cell reference includes a worksheet reference - @MarkBaker +- Ignore inlineStr type if formula element exists - @ncrypthic [#570](https://github.com/PHPOffice/PHPExcel/issues/570) +- Excel 2007 Reader freezes because of conditional formatting - @rentalhost [#575](https://github.com/PHPOffice/PHPExcel/issues/575) +- Readers will now parse files containing worksheet titles over 31 characters [#176](https://github.com/PHPOffice/PhpSpreadsheet/pull/176) +- Fixed PHP8 deprecation warning for libxml_disable_entity_loader() [#1625](https://github.com/phpoffice/phpspreadsheet/pull/1625) + +### General + +- Whitespace after toRichTextObject() - @MarkBaker [#554](https://github.com/PHPOffice/PHPExcel/issues/554) +- Optimize vlookup() sort - @umpirsky [#548](https://github.com/PHPOffice/PHPExcel/issues/548) +- c:max and c:min elements shall NOT be inside c:orientation elements - @vitalyrepin [#869](https://github.com/PHPOffice/PHPExcel/pull/869) +- Implement actual timezone adjustment into PHPExcel_Shared_Date::PHPToExcel - @sim642 [#489](https://github.com/PHPOffice/PHPExcel/pull/489) + +### BREAKING CHANGE + +- Introduction of namespaces for all classes, eg: `PHPExcel_Calculation_Functions` becomes `PhpOffice\PhpSpreadsheet\Calculation\Functions` +- Some classes were renamed for clarity and/or consistency: + +For a comprehensive list of all class changes, and a semi-automated migration path, read the [migration guide](./docs/topics/migration-from-PHPExcel.md). + +- Dropped `PHPExcel_Calculation_Functions::VERSION()`. Composer or git should be used to know the version. +- Dropped `PHPExcel_Settings::setPdfRenderer()` and `PHPExcel_Settings::setPdfRenderer()`. Composer should be used to autoload PDF libs. +- Dropped support for HHVM + +## Previous versions of PHPExcel + +The changelog for the project when it was called PHPExcel is [still available](./CHANGELOG.PHPExcel.md). + +### Changed +- Replace ezyang/htmlpurifier (LGPL2.1) with voku/anti-xss (MIT) diff --git a/application/third_party/PhpSpreadsheet-5.3.0/CONTRIBUTING.md b/application/third_party/PhpSpreadsheet-5.3.0/CONTRIBUTING.md new file mode 100644 index 0000000..6cbf9fb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Want to contribute? + +If you would like to contribute, here are some notes and guidelines: + + - All new development should be on feature/fix branches, which are then merged to the `master` branch once stable and approved; so the `master` branch is always the most up-to-date, working code + - If you are going to submit a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number + - Install (development) dependencies by running `composer install` inside your PhpSpreadsheet clone. + - The code must work with all PHP versions that we support. + - You can call `composer versions` to test version compatibility. + - Code style should be maintained. + - `composer style` will identify any issues with Coding Style. + - `composer fix` will fix most issues with Coding Style. + - All code changes must be validated by `composer check`. + - Please include Unit Tests to verify that a bug exists, and that this PR fixes it. + - Please include Unit Tests to show that a new Feature works as expected. + - Please don't "bundle" several changes into a single PR; submit a PR for each discrete change/fix. + - Remember to update documentation if necessary. + + - [Helpful article about forking](https://help.github.com/articles/fork-a-repo/ "Forking a GitHub repository") + - [Helpful article about pull requests](https://help.github.com/articles/using-pull-requests/ "Pull Requests") + +## Unit Tests + +When writing Unit Tests, please + - Always try to write Unit Tests for both the happy and unhappy paths. + - Put all assertions in the Test itself, not in an abstract class that the Test extends (even if this means code duplication between tests). + - Include any necessary `setup()` and `tearDown()` in the Test itself. + - If you change any global settings (such as system locale, or Compatibility Mode for Excel Function tests), make sure that you reset to the default in the `tearDown()`. + - Use the `ExcelError` functions in assertions for Excel Error values in Excel Function implementations. +
Not only does it reduce the risk of typos; but at some point in the future, ExcelError values will be an object rather than a string, and we won't then need to update all the tests. + - Don't over-complicate test code by testing happy and unhappy paths in the same test. + +This makes it easier to see exactly what is being tested when reviewing the PR. I want to be able to see it in the PR, not have to hunt in other unchanged classes to see what the test is doing. + +## How to release + +1. Complete CHANGELOG.md and commit +2. Create an annotated tag + 1. `git tag -a 1.2.3` + 2. Tag subject must be the version number, eg: `1.2.3` + 3. Tag body must be a copy-paste of the changelog entries. +3. Push the tag with `git push --tags`, GitHub Actions will create a GitHub release automatically, and the release details will automatically be sent to packagist. +4. By default, Github removes markdown headings in the Release Notes. You can either edit to restore these, or, probably preferably, change the default comment character on your system - `git config core.commentChar ";"`. + +> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.) However, there are 4 branches which have been updated to apply security patches, and those may be tagged if future security updates are needed. +- release1291 (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility) +- release210 (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility) +- release222 +- release390 diff --git a/application/third_party/PhpSpreadsheet-5.3.0/LICENSE b/application/third_party/PhpSpreadsheet-5.3.0/LICENSE new file mode 100644 index 0000000..04a90f0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2025 PhpSpreadsheet Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/application/third_party/PhpSpreadsheet-5.3.0/README.md b/application/third_party/PhpSpreadsheet-5.3.0/README.md new file mode 100644 index 0000000..8286e3a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/README.md @@ -0,0 +1,80 @@ +# PhpSpreadsheet + +[![Build Status](https://github.com/PHPOffice/PhpSpreadsheet/workflows/main/badge.svg)](https://github.com/PHPOffice/PhpSpreadsheet/actions) +[![Code Coverage](https://coveralls.io/repos/github/PHPOffice/PhpSpreadsheet/badge.svg?branch=master)](https://coveralls.io/github/PHPOffice/PhpSpreadsheet?branch=master) +[![Total Downloads](https://img.shields.io/packagist/dt/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) +[![Latest Stable Version](https://img.shields.io/github/v/release/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) +[![License](https://img.shields.io/github/license/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet) +[![Join the chat at https://gitter.im/PHPOffice/PhpSpreadsheet](https://img.shields.io/badge/GITTER-join%20chat-green.svg)](https://gitter.im/PHPOffice/PhpSpreadsheet) + +PhpSpreadsheet is a library written in pure PHP and offers a set of classes that +allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc. + +This is the master branch, and is maintained for security and bug fixes. + +## PHP Version Support + +LTS: For maintained branches, support for PHP versions will only be maintained for a period of six months beyond the +[end of life](https://www.php.net/supported-versions) of that PHP version. + +Currently the required PHP minimum version is PHP __8.1__, and we [will support that version](https://www.php.net/supported-versions.php) until 30th June 2026. + +See the `composer.json` for other requirements. + +## Installation + +See the [install instructions](https://phpspreadsheet.readthedocs.io/en/latest/#installation). + +## Documentation + +Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). + +Please ask your support questions on [StackOverflow](https://stackoverflow.com/questions/tagged/phpspreadsheet), or have a quick chat on [Gitter](https://gitter.im/PHPOffice/PhpSpreadsheet). + +## Patreon + +I am now running a [Patreon](https://www.patreon.com/MarkBaker) to support the work that I do on PhpSpreadsheet. + +Supporters will receive access to articles about working with PhpSpreadsheet, and how to use some of its more advanced features. + +Posts already available to Patreon supporters: + - The Dating Game + - A look at how MS Excel (and PhpSpreadsheet) handle date and time values. +- Looping the Loop + - Advice on Iterating through the rows and cells in a worksheet. + +And for Patrons at levels actively using PhpSpreadsheet: + - Behind the Mask + - A look at Number Format Masks. + +The Next Article (currently Work in Progress): + - Formula for Success + - How to debug formulae that don't produce the expected result. + + +My aim is to post at least one article each month, taking a detailed look at some feature of MS Excel and how to use that feature in PhpSpreadsheet, or on how to perform different activities in PhpSpreadsheet. + +Planned posts for the future include topics like: + - Tables + - Structured References + - AutoFiltering + - Array Formulae + - Conditional Formatting + - Data Validation + - Value Binders + - Images + - Charts + +After a period of six months exclusive to Patreon supporters, articles will be incorporated into the public documentation for the library. + +## PHPExcel vs PhpSpreadsheet ? + +PhpSpreadsheet is the next version of PHPExcel. It breaks compatibility to dramatically improve the code base quality (namespaces, PSR compliance, use of latest PHP language features, etc.). + +Because all efforts have shifted to PhpSpreadsheet, PHPExcel will no longer be maintained. All contributions for PHPExcel, patches and new features, should target PhpSpreadsheet `master` branch. + +Do you need to migrate? There is [an automated tool](/docs/topics/migration-from-PHPExcel.md) for that. + +## License + +PhpSpreadsheet is licensed under [MIT](https://github.com/PHPOffice/PhpSpreadsheet/blob/master/LICENSE). diff --git a/application/third_party/PhpSpreadsheet-5.3.0/composer.json b/application/third_party/PhpSpreadsheet-5.3.0/composer.json new file mode 100644 index 0000000..a0cced8 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/composer.json @@ -0,0 +1,123 @@ +{ + "name": "phpoffice/phpspreadsheet", + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "keywords": [ + "PHP", + "OpenXML", + "Excel", + "xlsx", + "xls", + "ods", + "gnumeric", + "spreadsheet" + ], + "config": { + "platform": { + "php" : "8.1.99" + }, + "process-timeout": 600, + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "scripts": { + "check": [ + "php bin/check-phpdoc-types.php", + "phpcs samples/ src/ tests/ --report=checkstyle", + "phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- --exclude=PHPCompatibility.Variables.ForbiddenThisUseContexts -n", + "php-cs-fixer fix --ansi --dry-run --diff", + "phpstan analyse --ansi --memory-limit=2048M", + "phpunit --color=always" + ], + "style": [ + "phpcs samples/ src/ tests/ --report=checkstyle", + "php-cs-fixer fix --ansi --dry-run --diff" + ], + "fix": [ + "phpcbf samples/ src/ tests/ --report=checkstyle", + "php-cs-fixer fix" + ], + "versions": [ + "phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- --exclude=PHPCompatibility.Variables.ForbiddenThisUseContexts -n" + ] + }, + "require": { + "php": "^8.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "composer/pcre": "^1||^2||^3", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.5", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" + }, + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "autoload-dev": { + "psr-4": { + "PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests", + "PhpOffice\\PhpSpreadsheetInfra\\": "infra" + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/ArrayEnabled.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/ArrayEnabled.php new file mode 100644 index 0000000..038b716 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/ArrayEnabled.php @@ -0,0 +1,134 @@ +initialise($arguments); + } + + /** + * Handles array argument processing when the function accepts a single argument that can be an array argument. + * Example use for: + * DAYOFMONTH() or FACT(). + * + * @param mixed[] $values + * + * @return mixed[] + */ + protected static function evaluateSingleArgumentArray(callable $method, array $values): array + { + $result = []; + foreach ($values as $value) { + $result[] = $method($value); + } + + return $result; + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * and any of them can be an array argument. + * Example use for: + * ROUND() or DATE(). + * + * @return mixed[] + */ + protected static function evaluateArrayArguments(callable $method, mixed ...$arguments): array + { + self::initialiseHelper($arguments); + $arguments = self::$arrayArgumentHelper->arguments(); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * but only the first few (up to limit) can be an array arguments. + * Example use for: + * NETWORKDAYS() or CONCATENATE(), where the last argument is a matrix (or a series of values) that need + * to be treated as a such rather than as an array arguments. + * + * @return mixed[] + */ + protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, mixed ...$arguments): array + { + self::initialiseHelper(array_slice($arguments, 0, $limit)); + $trailingArguments = array_slice($arguments, $limit); + $arguments = self::$arrayArgumentHelper->arguments(); + $arguments = array_merge($arguments, $trailingArguments); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } + + private static function testFalse(mixed $value): bool + { + return $value === false; + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * but only the last few (from start) can be an array arguments. + * Example use for: + * Z.TEST() or INDEX(), where the first argument 1 is a matrix that needs to be treated as a dataset + * rather than as an array argument. + * + * @return mixed[] + */ + protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, mixed ...$arguments): array + { + $arrayArgumentsSubset = array_combine( + range($start, count($arguments) - $start), + array_slice($arguments, $start) + ); + if (self::testFalse($arrayArgumentsSubset)) { + return ['#VALUE!']; + } + + self::initialiseHelper($arrayArgumentsSubset); + $leadingArguments = array_slice($arguments, 0, $start); + $arguments = self::$arrayArgumentHelper->arguments(); + $arguments = array_merge($leadingArguments, $arguments); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * and any of them can be an array argument except for the one specified by ignore. + * Example use for: + * HLOOKUP() and VLOOKUP(), where argument 1 is a matrix that needs to be treated as a database + * rather than as an array argument. + * + * @return mixed[] + */ + protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, mixed ...$arguments): array + { + $leadingArguments = array_slice($arguments, 0, $ignore); + $ignoreArgument = array_slice($arguments, $ignore, 1); + $trailingArguments = array_slice($arguments, $ignore + 1); + + self::initialiseHelper(array_merge($leadingArguments, [[null]], $trailingArguments)); + $arguments = self::$arrayArgumentHelper->arguments(); + + array_splice($arguments, $ignore, 1, $ignoreArgument); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/BinaryComparison.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/BinaryComparison.php new file mode 100644 index 0000000..2edd5de --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/BinaryComparison.php @@ -0,0 +1,141 @@ + '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) { + $operand1 = Calculation::unwrapResult($operand1); + } + if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) { + $operand2 = Calculation::unwrapResult($operand2); + } + + // Use case-insensitive comparison if not OpenOffice mode + if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) { + if (is_string($operand1)) { + $operand1 = StringHelper::strToUpper($operand1); + } + if (is_string($operand2)) { + $operand2 = StringHelper::strToUpper($operand2); + } + } + + $useLowercaseFirstComparison = is_string($operand1) + && is_string($operand2) + && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE; + + return self::evaluateComparison($operand1, $operand2, $operator, $useLowercaseFirstComparison); + } + + private static function evaluateComparison(mixed $operand1, mixed $operand2, string $operator, bool $useLowercaseFirstComparison): bool + { + return match ($operator) { + '=' => self::equal($operand1, $operand2), + '>' => self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison), + '<' => self::lessThan($operand1, $operand2, $useLowercaseFirstComparison), + '>=' => self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison), + '<=' => self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison), + '<>' => self::notEqual($operand1, $operand2), + default => throw new Exception('Unsupported binary comparison operator'), + }; + } + + private static function equal(mixed $operand1, mixed $operand2): bool + { + if (is_numeric($operand1) && is_numeric($operand2)) { + $result = (abs($operand1 - $operand2) < self::DELTA); + } elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { + $result = $operand1 == $operand2; + } else { + $result = self::strcmpAllowNull($operand1, $operand2) == 0; + } + + return $result; + } + + private static function greaterThanOrEqual(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool + { + if (is_numeric($operand1) && is_numeric($operand2)) { + $result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 > $operand2)); + } elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { + $result = $operand1 >= $operand2; + } elseif ($useLowercaseFirstComparison) { + $result = self::strcmpLowercaseFirst($operand1, $operand2) >= 0; + } else { + $result = self::strcmpAllowNull($operand1, $operand2) >= 0; + } + + return $result; + } + + private static function lessThanOrEqual(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool + { + if (is_numeric($operand1) && is_numeric($operand2)) { + $result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 < $operand2)); + } elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { + $result = $operand1 <= $operand2; + } elseif ($useLowercaseFirstComparison) { + $result = self::strcmpLowercaseFirst($operand1, $operand2) <= 0; + } else { + $result = self::strcmpAllowNull($operand1, $operand2) <= 0; + } + + return $result; + } + + private static function greaterThan(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool + { + return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true; + } + + private static function lessThan(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool + { + return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true; + } + + private static function notEqual(mixed $operand1, mixed $operand2): bool + { + return self::equal($operand1, $operand2) !== true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Calculation.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Calculation.php new file mode 100644 index 0000000..8320780 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Calculation.php @@ -0,0 +1,2930 @@ +=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; + // Used only to detect spill operator # + const CALCULATION_REGEXP_CELLREF_SPILL = '/' . self::CALCULATION_REGEXP_CELLREF . '#/i'; + // Cell reference (with or without a sheet reference) ensuring absolute/relative + const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; + const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; + // Cell reference (with or without a sheet reference) ensuring absolute/relative + // Cell ranges ensuring absolute/relative + const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; + const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})'; + // Defined Names: Named Range of cells, or Named Formulae + const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; + // Structured Reference (Fully Qualified and Unqualified) + const CALCULATION_REGEXP_STRUCTURED_REFERENCE = '([\p{L}_\\\][\p{L}\p{N}\._]+)?(\[(?:[^\d\]+-])?)'; + // Error + const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; + + /** constants */ + const RETURN_ARRAY_AS_ERROR = 'error'; + const RETURN_ARRAY_AS_VALUE = 'value'; + const RETURN_ARRAY_AS_ARRAY = 'array'; + + /** Preferable to use instance variable instanceArrayReturnType rather than this static property. */ + private static string $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE; + + /** Preferable to use this instance variable rather than static returnArrayAsType */ + private ?string $instanceArrayReturnType = null; + + /** + * Instance of this class. + */ + private static ?Calculation $instance = null; + + /** + * Instance of the spreadsheet this Calculation Engine is using. + */ + private ?Spreadsheet $spreadsheet; + + /** + * Calculation cache. + * + * @var mixed[] + */ + private array $calculationCache = []; + + /** + * Calculation cache enabled. + */ + private bool $calculationCacheEnabled = true; + + private BranchPruner $branchPruner; + + private bool $branchPruningEnabled = true; + + /** + * List of operators that can be used within formulae + * The true/false value indicates whether it is a binary operator or a unary operator. + */ + private const CALCULATION_OPERATORS = [ + '+' => true, '-' => true, '*' => true, '/' => true, + '^' => true, '&' => true, '%' => false, '~' => false, + '>' => true, '<' => true, '=' => true, '>=' => true, + '<=' => true, '<>' => true, '∩' => true, '∪' => true, + ':' => true, + ]; + + /** + * List of binary operators (those that expect two operands). + */ + private const BINARY_OPERATORS = [ + '+' => true, '-' => true, '*' => true, '/' => true, + '^' => true, '&' => true, '>' => true, '<' => true, + '=' => true, '>=' => true, '<=' => true, '<>' => true, + '∩' => true, '∪' => true, ':' => true, + ]; + + /** + * The debug log generated by the calculation engine. + */ + private Logger $debugLog; + + private bool $suppressFormulaErrors = false; + + private bool $processingAnchorArray = false; + + /** + * Error message for any error that was raised/thrown by the calculation engine. + */ + public ?string $formulaError = null; + + /** + * An array of the nested cell references accessed by the calculation engine, used for the debug log. + */ + private CyclicReferenceStack $cyclicReferenceStack; + + /** @var mixed[] */ + private array $cellStack = []; + + /** + * Current iteration counter for cyclic formulae + * If the value is 0 (or less) then cyclic formulae will throw an exception, + * otherwise they will iterate to the limit defined here before returning a result. + */ + private int $cyclicFormulaCounter = 1; + + private string $cyclicFormulaCell = ''; + + /** + * Number of iterations for cyclic formulae. + */ + public int $cyclicFormulaCount = 1; + + /** + * Excel constant string translations to their PHP equivalents + * Constant conversion from text name/value to actual (datatyped) value. + */ + private const EXCEL_CONSTANTS = [ + 'TRUE' => true, + 'FALSE' => false, + 'NULL' => null, + ]; + + public static function keyInExcelConstants(string $key): bool + { + return array_key_exists($key, self::EXCEL_CONSTANTS); + } + + public static function getExcelConstants(string $key): bool|null + { + return self::EXCEL_CONSTANTS[$key]; + } + + /** + * Internal functions used for special control purposes. + * + * @var array|string>> + */ + private static array $controlFunctions = [ + 'MKMATRIX' => [ + 'argumentCount' => '*', + 'functionCall' => [Internal\MakeMatrix::class, 'make'], + ], + 'NAME.ERROR' => [ + 'argumentCount' => '*', + 'functionCall' => [ExcelError::class, 'NAME'], + ], + 'WILDCARDMATCH' => [ + 'argumentCount' => '2', + 'functionCall' => [Internal\WildcardMatch::class, 'compare'], + ], + ]; + + public function __construct(?Spreadsheet $spreadsheet = null) + { + $this->spreadsheet = $spreadsheet; + $this->cyclicReferenceStack = new CyclicReferenceStack(); + $this->debugLog = new Logger($this->cyclicReferenceStack); + $this->branchPruner = new BranchPruner($this->branchPruningEnabled); + } + + /** + * Get an instance of this class. + * + * @param ?Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object, + * or NULL to create a standalone calculation engine + */ + public static function getInstance(?Spreadsheet $spreadsheet = null): self + { + if ($spreadsheet !== null) { + return $spreadsheet->getCalculationEngine(); + } + + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Intended for use only via a destructor. + * + * @internal + */ + public static function getInstanceOrNull(?Spreadsheet $spreadsheet = null): ?self + { + if ($spreadsheet !== null) { + return $spreadsheet->getCalculationEngineOrNull(); + } + + return null; + } + + /** + * Flush the calculation cache for any existing instance of this class + * but only if a Calculation instance exists. + */ + public function flushInstance(): void + { + $this->clearCalculationCache(); + $this->branchPruner->clearBranchStore(); + } + + /** + * Get the Logger for this calculation engine instance. + */ + public function getDebugLog(): Logger + { + return $this->debugLog; + } + + /** + * __clone implementation. Cloning should not be allowed in a Singleton! + */ + final public function __clone() + { + throw new Exception('Cloning the calculation engine is not allowed!'); + } + + /** + * Set the Array Return Type (Array or Value of first element in the array). + * + * @param string $returnType Array return type + * + * @return bool Success or failure + */ + public static function setArrayReturnType(string $returnType): bool + { + if ( + ($returnType == self::RETURN_ARRAY_AS_VALUE) + || ($returnType == self::RETURN_ARRAY_AS_ERROR) + || ($returnType == self::RETURN_ARRAY_AS_ARRAY) + ) { + self::$returnArrayAsType = $returnType; + + return true; + } + + return false; + } + + /** + * Return the Array Return Type (Array or Value of first element in the array). + * + * @return string $returnType Array return type + */ + public static function getArrayReturnType(): string + { + return self::$returnArrayAsType; + } + + /** + * Set the Instance Array Return Type (Array or Value of first element in the array). + * + * @param string $returnType Array return type + * + * @return bool Success or failure + */ + public function setInstanceArrayReturnType(string $returnType): bool + { + if ( + ($returnType == self::RETURN_ARRAY_AS_VALUE) + || ($returnType == self::RETURN_ARRAY_AS_ERROR) + || ($returnType == self::RETURN_ARRAY_AS_ARRAY) + ) { + $this->instanceArrayReturnType = $returnType; + + return true; + } + + return false; + } + + /** + * Return the Array Return Type (Array or Value of first element in the array). + * + * @return string $returnType Array return type for instance if non-null, otherwise static property + */ + public function getInstanceArrayReturnType(): string + { + return $this->instanceArrayReturnType ?? self::$returnArrayAsType; + } + + /** + * Is calculation caching enabled? + */ + public function getCalculationCacheEnabled(): bool + { + return $this->calculationCacheEnabled; + } + + /** + * Enable/disable calculation cache. + */ + public function setCalculationCacheEnabled(bool $calculationCacheEnabled): self + { + $this->calculationCacheEnabled = $calculationCacheEnabled; + $this->clearCalculationCache(); + + return $this; + } + + /** + * Enable calculation cache. + */ + public function enableCalculationCache(): void + { + $this->setCalculationCacheEnabled(true); + } + + /** + * Disable calculation cache. + */ + public function disableCalculationCache(): void + { + $this->setCalculationCacheEnabled(false); + } + + /** + * Clear calculation cache. + */ + public function clearCalculationCache(): void + { + $this->calculationCache = []; + } + + /** + * Clear calculation cache for a specified worksheet. + */ + public function clearCalculationCacheForWorksheet(string $worksheetName): void + { + if (isset($this->calculationCache[$worksheetName])) { + unset($this->calculationCache[$worksheetName]); + } + } + + /** + * Rename calculation cache for a specified worksheet. + */ + public function renameCalculationCacheForWorksheet(string $fromWorksheetName, string $toWorksheetName): void + { + if (isset($this->calculationCache[$fromWorksheetName])) { + $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName]; + unset($this->calculationCache[$fromWorksheetName]); + } + } + + public function getBranchPruningEnabled(): bool + { + return $this->branchPruningEnabled; + } + + public function setBranchPruningEnabled(mixed $enabled): self + { + $this->branchPruningEnabled = (bool) $enabled; + $this->branchPruner = new BranchPruner($this->branchPruningEnabled); + + return $this; + } + + public function enableBranchPruning(): void + { + $this->setBranchPruningEnabled(true); + } + + public function disableBranchPruning(): void + { + $this->setBranchPruningEnabled(false); + } + + /** + * Wrap string values in quotes. + */ + public static function wrapResult(mixed $value): mixed + { + if (is_string($value)) { + // Error values cannot be "wrapped" + if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) { + // Return Excel errors "as is" + return $value; + } + + // Return strings wrapped in quotes + return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE; + } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) { + // Convert numeric errors to NaN error + return ExcelError::NAN(); + } + + return $value; + } + + /** + * Remove quotes used as a wrapper to identify string values. + */ + public static function unwrapResult(mixed $value): mixed + { + if (is_string($value)) { + if ((isset($value[0])) && ($value[0] == self::FORMULA_STRING_QUOTE) && (substr($value, -1) == self::FORMULA_STRING_QUOTE)) { + return substr($value, 1, -1); + } + // Convert numeric errors to NAN error + } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) { + return ExcelError::NAN(); + } + + return $value; + } + + /** + * Calculate cell value (using formula from a cell ID) + * Retained for backward compatibility. + * + * @param ?Cell $cell Cell to calculate + */ + public function calculate(?Cell $cell = null): mixed + { + try { + return $this->calculateCellValue($cell); + } catch (\Exception $e) { + throw new Exception($e->getMessage()); + } + } + + /** + * Calculate the value of a cell formula. + * + * @param ?Cell $cell Cell to calculate + * @param bool $resetLog Flag indicating whether the debug log should be reset or not + */ + public function calculateCellValue(?Cell $cell = null, bool $resetLog = true): mixed + { + if ($cell === null) { + return null; + } + + if ($resetLog) { + // Initialise the logging settings if requested + $this->formulaError = null; + $this->debugLog->clearLog(); + $this->cyclicReferenceStack->clear(); + $this->cyclicFormulaCounter = 1; + } + + // Execute the calculation for the cell formula + $this->cellStack[] = [ + 'sheet' => $cell->getWorksheet()->getTitle(), + 'cell' => $cell->getCoordinate(), + ]; + + $cellAddressAttempted = false; + $cellAddress = null; + + try { + $value = $cell->getValue(); + if (is_string($value) && $cell->getDataType() === DataType::TYPE_FORMULA) { + $value = preg_replace_callback( + self::CALCULATION_REGEXP_CELLREF_SPILL, + fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')', + $value + ); + } + $result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore-line + if ($this->spreadsheet === null) { + throw new Exception('null spreadsheet in calculateCellValue'); + } + $cellAddressAttempted = true; + $cellAddress = array_pop($this->cellStack); + if ($cellAddress === null) { + throw new Exception('null cellAddress in calculateCellValue'); + } + /** @var array{sheet: string, cell: string} $cellAddress */ + $testSheet = $this->spreadsheet->getSheetByName($cellAddress['sheet']); + if ($testSheet === null) { + throw new Exception('worksheet not found in calculateCellValue'); + } + $testSheet->getCell($cellAddress['cell']); + } catch (\Exception $e) { + if (!$cellAddressAttempted) { + $cellAddress = array_pop($this->cellStack); + } + if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) { + $sheetName = $cellAddress['sheet'] ?? null; + $testSheet = is_string($sheetName) ? $this->spreadsheet->getSheetByName($sheetName) : null; + if ($testSheet !== null && array_key_exists('cell', $cellAddress)) { + /** @var array{cell: string} $cellAddress */ + $testSheet->getCell($cellAddress['cell']); + } + } + + throw new Exception($e->getMessage(), $e->getCode(), $e); + } + + if (is_array($result) && $this->getInstanceArrayReturnType() !== self::RETURN_ARRAY_AS_ARRAY) { + $testResult = Functions::flattenArray($result); + if ($this->getInstanceArrayReturnType() == self::RETURN_ARRAY_AS_ERROR) { + return ExcelError::VALUE(); + } + $result = array_shift($testResult); + } + + if ($result === null && $cell->getWorksheet()->getSheetView()->getShowZeros()) { + return 0; + } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) { + return ExcelError::NAN(); + } + + return $result; + } + + /** + * Validate and parse a formula string. + * + * @param string $formula Formula to parse + * + * @return array|bool + */ + public function parseFormula(string $formula): array|bool + { + $formula = preg_replace_callback( + self::CALCULATION_REGEXP_CELLREF_SPILL, + fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')', + $formula + ) ?? $formula; + // Basic validation that this is indeed a formula + // We return an empty array if not + $formula = trim($formula); + if ((!isset($formula[0])) || ($formula[0] != '=')) { + return []; + } + $formula = ltrim(substr($formula, 1)); + if (!isset($formula[0])) { + return []; + } + + // Parse the formula and return the token stack + return $this->internalParseFormula($formula); + } + + /** + * Calculate the value of a formula. + * + * @param string $formula Formula to parse + * @param ?string $cellID Address of the cell to calculate + * @param ?Cell $cell Cell to calculate + */ + public function calculateFormula(string $formula, ?string $cellID = null, ?Cell $cell = null): mixed + { + // Initialise the logging settings + $this->formulaError = null; + $this->debugLog->clearLog(); + $this->cyclicReferenceStack->clear(); + + $resetCache = $this->getCalculationCacheEnabled(); + if ($this->spreadsheet !== null && $cellID === null && $cell === null) { + $cellID = 'A1'; + $cell = $this->spreadsheet->getActiveSheet()->getCell($cellID); + } else { + // Disable calculation cacheing because it only applies to cell calculations, not straight formulae + // But don't actually flush any cache + $this->calculationCacheEnabled = false; + } + + // Execute the calculation + try { + $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $cell)); + } catch (\Exception $e) { + throw new Exception($e->getMessage()); + } + + if ($this->spreadsheet === null) { + // Reset calculation cacheing to its previous state + $this->calculationCacheEnabled = $resetCache; + } + + return $result; + } + + public function getValueFromCache(string $cellReference, mixed &$cellValue): bool + { + $this->debugLog->writeDebugLog('Testing cache value for cell %s', $cellReference); + // Is calculation cacheing enabled? + // If so, is the required value present in calculation cache? + if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) { + $this->debugLog->writeDebugLog('Retrieving value for cell %s from cache', $cellReference); + // Return the cached result + + $cellValue = $this->calculationCache[$cellReference]; + + return true; + } + + return false; + } + + public function saveValueToCache(string $cellReference, mixed $cellValue): void + { + if ($this->calculationCacheEnabled) { + $this->calculationCache[$cellReference] = $cellValue; + } + } + + /** + * Parse a cell formula and calculate its value. + * + * @param string $formula The formula to parse and calculate + * @param ?string $cellID The ID (e.g. A3) of the cell that we are calculating + * @param ?Cell $cell Cell to calculate + * @param bool $ignoreQuotePrefix If set to true, evaluate the formyla even if the referenced cell is quote prefixed + */ + public function _calculateFormulaValue(string $formula, ?string $cellID = null, ?Cell $cell = null, bool $ignoreQuotePrefix = false): mixed + { + $cellValue = null; + + // Quote-Prefixed cell values cannot be formulae, but are treated as strings + if ($cell !== null && $ignoreQuotePrefix === false && $cell->getStyle()->getQuotePrefix() === true) { + return self::wrapResult((string) $formula); + } + + if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) { + return self::wrapResult($formula); + } + + // Basic validation that this is indeed a formula + // We simply return the cell value if not + $formula = trim($formula); + if ($formula === '' || $formula[0] !== '=') { + return self::wrapResult($formula); + } + $formula = ltrim(substr($formula, 1)); + if (!isset($formula[0])) { + return self::wrapResult($formula); + } + + $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null; + $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk"; + $wsCellReference = $wsTitle . '!' . $cellID; + + if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) { + return $cellValue; + } + $this->debugLog->writeDebugLog('Evaluating formula for cell %s', $wsCellReference); + + if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) { + if ($this->cyclicFormulaCount <= 0) { + $this->cyclicFormulaCell = ''; + + return $this->raiseFormulaError('Cyclic Reference in Formula'); + } elseif ($this->cyclicFormulaCell === $wsCellReference) { + ++$this->cyclicFormulaCounter; + if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) { + $this->cyclicFormulaCell = ''; + + return $cellValue; + } + } elseif ($this->cyclicFormulaCell == '') { + if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) { + return $cellValue; + } + $this->cyclicFormulaCell = $wsCellReference; + } + } + + $this->debugLog->writeDebugLog('Formula for cell %s is %s', $wsCellReference, $formula); + // Parse the formula onto the token stack and calculate the value + $this->cyclicReferenceStack->push($wsCellReference); + + $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $cell), $cellID, $cell); + $this->cyclicReferenceStack->pop(); + + // Save to calculation cache + if ($cellID !== null) { + $this->saveValueToCache($wsCellReference, $cellValue); + } + + // Return the calculated value + return $cellValue; + } + + /** + * Ensure that paired matrix operands are both matrices and of the same size. + * + * @param mixed $operand1 First matrix operand + * + * @param-out mixed[] $operand1 + * + * @param mixed $operand2 Second matrix operand + * + * @param-out mixed[] $operand2 + * + * @param int $resize Flag indicating whether the matrices should be resized to match + * and (if so), whether the smaller dimension should grow or the + * larger should shrink. + * 0 = no resize + * 1 = shrink to fit + * 2 = extend to fit + * + * @return mixed[] + */ + public static function checkMatrixOperands(mixed &$operand1, mixed &$operand2, int $resize = 1): array + { + // Examine each of the two operands, and turn them into an array if they aren't one already + // Note that this function should only be called if one or both of the operand is already an array + if (!is_array($operand1)) { + if (is_array($operand2)) { + [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand2); + $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1)); + $resize = 0; + } else { + $operand1 = [$operand1]; + $operand2 = [$operand2]; + } + } elseif (!is_array($operand2)) { + [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand1); + $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2)); + $resize = 0; + } + + [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1); + [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2); + if ($resize === 3) { + $resize = 2; + } elseif (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) { + $resize = 1; + } + + if ($resize == 2) { + // Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger + self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns); + } elseif ($resize == 1) { + // Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller + /** @var mixed[][] $operand1 */ + /** @var mixed[][] $operand2 */ + self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns); + } + [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1); + [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2); + + return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns]; + } + + /** + * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0. + * + * @param mixed[] $matrix matrix operand + * + * @return int[] An array comprising the number of rows, and number of columns + */ + public static function getMatrixDimensions(array &$matrix): array + { + $matrixRows = count($matrix); + $matrixColumns = 0; + foreach ($matrix as $rowKey => $rowValue) { + if (!is_array($rowValue)) { + $matrix[$rowKey] = [$rowValue]; + $matrixColumns = max(1, $matrixColumns); + } else { + $matrix[$rowKey] = array_values($rowValue); + $matrixColumns = max(count($rowValue), $matrixColumns); + } + } + $matrix = array_values($matrix); + + return [$matrixRows, $matrixColumns]; + } + + /** + * Ensure that paired matrix operands are both matrices of the same size. + * + * @param mixed[][] $matrix1 First matrix operand + * @param mixed[][] $matrix2 Second matrix operand + * @param int $matrix1Rows Row size of first matrix operand + * @param int $matrix1Columns Column size of first matrix operand + * @param int $matrix2Rows Row size of second matrix operand + * @param int $matrix2Columns Column size of second matrix operand + */ + private static function resizeMatricesShrink(array &$matrix1, array &$matrix2, int $matrix1Rows, int $matrix1Columns, int $matrix2Rows, int $matrix2Columns): void + { + if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) { + if ($matrix2Rows < $matrix1Rows) { + for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) { + unset($matrix1[$i]); + } + } + if ($matrix2Columns < $matrix1Columns) { + for ($i = 0; $i < $matrix1Rows; ++$i) { + for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) { + unset($matrix1[$i][$j]); + } + } + } + } + + if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) { + if ($matrix1Rows < $matrix2Rows) { + for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) { + unset($matrix2[$i]); + } + } + if ($matrix1Columns < $matrix2Columns) { + for ($i = 0; $i < $matrix2Rows; ++$i) { + for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) { + unset($matrix2[$i][$j]); + } + } + } + } + } + + /** + * Ensure that paired matrix operands are both matrices of the same size. + * + * @param mixed[] $matrix1 First matrix operand + * @param mixed[] $matrix2 Second matrix operand + * @param int $matrix1Rows Row size of first matrix operand + * @param int $matrix1Columns Column size of first matrix operand + * @param int $matrix2Rows Row size of second matrix operand + * @param int $matrix2Columns Column size of second matrix operand + */ + private static function resizeMatricesExtend(array &$matrix1, array &$matrix2, int $matrix1Rows, int $matrix1Columns, int $matrix2Rows, int $matrix2Columns): void + { + if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) { + if ($matrix2Columns < $matrix1Columns) { + for ($i = 0; $i < $matrix2Rows; ++$i) { + /** @var mixed[][] $matrix2 */ + $x = ($matrix2Columns === 1) ? $matrix2[$i][0] : null; + for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) { + $matrix2[$i][$j] = $x; + } + } + } + if ($matrix2Rows < $matrix1Rows) { + $x = ($matrix2Rows === 1) ? $matrix2[0] : array_fill(0, $matrix2Columns, null); + for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) { + $matrix2[$i] = $x; + } + } + } + + if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) { + if ($matrix1Columns < $matrix2Columns) { + for ($i = 0; $i < $matrix1Rows; ++$i) { + /** @var mixed[][] $matrix1 */ + $x = ($matrix1Columns === 1) ? $matrix1[$i][0] : null; + for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) { + $matrix1[$i][$j] = $x; + } + } + } + if ($matrix1Rows < $matrix2Rows) { + $x = ($matrix1Rows === 1) ? $matrix1[0] : array_fill(0, $matrix2Columns, null); + for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) { + $matrix1[$i] = $x; + } + } + } + } + + /** + * Format details of an operand for display in the log (based on operand type). + * + * @param mixed $value First matrix operand + */ + private function showValue(mixed $value): mixed + { + if ($this->debugLog->getWriteDebugLog()) { + $testArray = Functions::flattenArray($value); + if (count($testArray) == 1) { + $value = array_pop($testArray); + } + + if (is_array($value)) { + $returnMatrix = []; + $pad = $rpad = ', '; + foreach ($value as $row) { + if (is_array($row)) { + $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); + $rpad = '; '; + } else { + $returnMatrix[] = $this->showValue($row); + } + } + + return '{ ' . implode($rpad, $returnMatrix) . ' }'; + } elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) { + return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE; + } elseif (is_bool($value)) { + return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE']; + } elseif ($value === null) { + return self::$localeBoolean['NULL']; + } + } + + return Functions::flattenSingleValue($value); + } + + /** + * Format type and details of an operand for display in the log (based on operand type). + * + * @param mixed $value First matrix operand + */ + private function showTypeDetails(mixed $value): ?string + { + if ($this->debugLog->getWriteDebugLog()) { + $testArray = Functions::flattenArray($value); + if (count($testArray) == 1) { + $value = array_pop($testArray); + } + + if ($value === null) { + return 'a NULL value'; + } elseif (is_float($value)) { + $typeString = 'a floating point number'; + } elseif (is_int($value)) { + $typeString = 'an integer number'; + } elseif (is_bool($value)) { + $typeString = 'a boolean'; + } elseif (is_array($value)) { + $typeString = 'a matrix'; + } else { + /** @var string $value */ + if ($value == '') { + return 'an empty string'; + } elseif ($value[0] == '#') { + return 'a ' . $value . ' error'; + } + $typeString = 'a string'; + } + + return $typeString . ' with a value of ' . StringHelper::convertToString($this->showValue($value)); + } + + return null; + } + + private const MATRIX_REPLACE_FROM = [self::FORMULA_OPEN_MATRIX_BRACE, ';', self::FORMULA_CLOSE_MATRIX_BRACE]; + private const MATRIX_REPLACE_TO = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))']; + + /** + * @return false|string False indicates an error + */ + private function convertMatrixReferences(string $formula): false|string + { + // Convert any Excel matrix references to the MKMATRIX() function + if (str_contains($formula, self::FORMULA_OPEN_MATRIX_BRACE)) { + // If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators + if (str_contains($formula, self::FORMULA_STRING_QUOTE)) { + // So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded + // the formula + $temp = explode(self::FORMULA_STRING_QUOTE, $formula); + // Open and Closed counts used for trapping mismatched braces in the formula + $openCount = $closeCount = 0; + $notWithinQuotes = false; + foreach ($temp as &$value) { + // Only count/replace in alternating array entries + $notWithinQuotes = $notWithinQuotes === false; + if ($notWithinQuotes === true) { + $openCount += substr_count($value, self::FORMULA_OPEN_MATRIX_BRACE); + $closeCount += substr_count($value, self::FORMULA_CLOSE_MATRIX_BRACE); + $value = str_replace(self::MATRIX_REPLACE_FROM, self::MATRIX_REPLACE_TO, $value); + } + } + unset($value); + // Then rebuild the formula string + $formula = implode(self::FORMULA_STRING_QUOTE, $temp); + } else { + // If there's no quoted strings, then we do a simple count/replace + $openCount = substr_count($formula, self::FORMULA_OPEN_MATRIX_BRACE); + $closeCount = substr_count($formula, self::FORMULA_CLOSE_MATRIX_BRACE); + $formula = str_replace(self::MATRIX_REPLACE_FROM, self::MATRIX_REPLACE_TO, $formula); + } + // Trap for mismatched braces and trigger an appropriate error + if ($openCount < $closeCount) { + if ($openCount > 0) { + return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'"); + } + + return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered"); + } elseif ($openCount > $closeCount) { + if ($closeCount > 0) { + return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'"); + } + + return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered"); + } + } + + return $formula; + } + + /** + * Comparison (Boolean) Operators. + * These operators work on two values, but always return a boolean result. + */ + private const COMPARISON_OPERATORS = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true]; + + /** + * Operator Precedence. + * This list includes all valid operators, whether binary (including boolean) or unary (such as %). + * Array key is the operator, the value is its precedence. + */ + private const OPERATOR_PRECEDENCE = [ + ':' => 9, // Range + '∩' => 8, // Intersect + '∪' => 7, // Union + '~' => 6, // Negation + '%' => 5, // Percentage + '^' => 4, // Exponentiation + '*' => 3, '/' => 3, // Multiplication and Division + '+' => 2, '-' => 2, // Addition and Subtraction + '&' => 1, // Concatenation + '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison + ]; + + /** + * @return array|false + */ + private function internalParseFormula(string $formula, ?Cell $cell = null): bool|array + { + if (($formula = $this->convertMatrixReferences(trim($formula))) === false) { + return false; + } + $phpSpreadsheetFunctions = &self::getFunctionsAddress(); + + // If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet), + // so we store the parent worksheet so that we can re-attach it when necessary + $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null; + + $regexpMatchString = '/^((?' . self::CALCULATION_REGEXP_STRING + . ')|(?' . self::CALCULATION_REGEXP_FUNCTION + . ')|(?' . self::CALCULATION_REGEXP_CELLREF + . ')|(?' . self::CALCULATION_REGEXP_COLUMN_RANGE + . ')|(?' . self::CALCULATION_REGEXP_ROW_RANGE + . ')|(?' . self::CALCULATION_REGEXP_NUMBER + . ')|(?' . self::CALCULATION_REGEXP_OPENBRACE + . ')|(?' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE + . ')|(?' . self::CALCULATION_REGEXP_DEFINEDNAME + . ')|(?' . self::CALCULATION_REGEXP_ERROR + . '))/sui'; + + // Start with initialisation + $index = 0; + $stack = new Stack($this->branchPruner); + $output = []; + $expectingOperator = false; // We use this test in syntax-checking the expression to determine when a + // - is a negation or + is a positive operator rather than an operation + $expectingOperand = false; // We use this test in syntax-checking the expression to determine whether an operand + // should be null in a function call + + // The guts of the lexical parser + // Loop through the formula extracting each operator and operand in turn + while (true) { + // Branch pruning: we adapt the output item to the context (it will + // be used to limit its computation) + $this->branchPruner->initialiseForLoop(); + + $opCharacter = $formula[$index]; // Get the first character of the value at the current index position + if ($opCharacter === "\xe2") { // intersection or union + $opCharacter .= $formula[++$index]; + $opCharacter .= $formula[++$index]; + } + + // Check for two-character operators (e.g. >=, <=, <>) + if ((isset(self::COMPARISON_OPERATORS[$opCharacter])) && (strlen($formula) > $index) && isset($formula[$index + 1], self::COMPARISON_OPERATORS[$formula[$index + 1]])) { + $opCharacter .= $formula[++$index]; + } + // Find out if we're currently at the beginning of a number, variable, cell/row/column reference, + // function, defined name, structured reference, parenthesis, error or operand + $isOperandOrFunction = (bool) preg_match($regexpMatchString, substr($formula, $index), $match); + + $expectingOperatorCopy = $expectingOperator; + if ($opCharacter === '-' && !$expectingOperator) { // Is it a negation instead of a minus? + // Put a negation on the stack + $stack->push('Unary Operator', '~'); + ++$index; // and drop the negation symbol + } elseif ($opCharacter === '%' && $expectingOperator) { + // Put a percentage on the stack + $stack->push('Unary Operator', '%'); + ++$index; + } elseif ($opCharacter === '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded? + ++$index; // Drop the redundant plus symbol + } elseif ((($opCharacter === '~') /*|| ($opCharacter === '∩') || ($opCharacter === '∪')*/) && (!$isOperandOrFunction)) { + // We have to explicitly deny a tilde, union or intersect because they are legal + return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression + } elseif ((isset(self::CALCULATION_OPERATORS[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack? + while (self::swapOperands($stack, $opCharacter)) { + $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output + } + + // Finally put our current operator onto the stack + $stack->push('Binary Operator', $opCharacter); + + ++$index; + $expectingOperator = false; + } elseif ($opCharacter === ')' && $expectingOperator) { // Are we expecting to close a parenthesis? + $expectingOperand = false; + while (($o2 = $stack->pop()) && $o2['value'] !== '(') { // Pop off the stack back to the last ( + $output[] = $o2; + } + $d = $stack->last(2); + + // Branch pruning we decrease the depth whether is it a function + // call or a parenthesis + $this->branchPruner->decrementDepth(); + + if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', StringHelper::convertToString($d['value']), $matches)) { + // Did this parenthesis just close a function? + try { + $this->branchPruner->closingBrace($d['value']); + } catch (Exception $e) { + return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e); + } + + $functionName = $matches[1]; // Get the function name + $d = $stack->pop(); + $argumentCount = $d['value'] ?? 0; // See how many arguments there were (argument count is the next value stored on the stack) + $output[] = $d; // Dump the argument count on the output + $output[] = $stack->pop(); // Pop the function and push onto the output + if (isset(self::$controlFunctions[$functionName])) { + $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount']; + } elseif (isset($phpSpreadsheetFunctions[$functionName])) { + $expectedArgumentCount = $phpSpreadsheetFunctions[$functionName]['argumentCount']; + } else { // did we somehow push a non-function on the stack? this should never happen + return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack'); + } + // Check the argument count + $argumentCountError = false; + $expectedArgumentCountString = null; + if (is_numeric($expectedArgumentCount)) { + if ($expectedArgumentCount < 0) { + if ($argumentCount > abs($expectedArgumentCount + 0)) { + $argumentCountError = true; + $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount + 0); + } + } else { + if ($argumentCount != $expectedArgumentCount) { + $argumentCountError = true; + $expectedArgumentCountString = $expectedArgumentCount; + } + } + } elseif (is_string($expectedArgumentCount) && $expectedArgumentCount !== '*') { + if (1 !== preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) { + $argMatch = ['', '', '', '']; + } + switch ($argMatch[2]) { + case '+': + if ($argumentCount < $argMatch[1]) { + $argumentCountError = true; + $expectedArgumentCountString = $argMatch[1] . ' or more '; + } + + break; + case '-': + if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) { + $argumentCountError = true; + $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3]; + } + + break; + case ',': + if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) { + $argumentCountError = true; + $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3]; + } + + break; + } + } + if ($argumentCountError) { + /** @var int $argumentCount */ + return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected'); + } + } + ++$index; + } elseif ($opCharacter === ',') { // Is this the separator for function arguments? + try { + $this->branchPruner->argumentSeparator(); + } catch (Exception $e) { + return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e); + } + + while (($o2 = $stack->pop()) && $o2['value'] !== '(') { // Pop off the stack back to the last ( + $output[] = $o2; // pop the argument expression stuff and push onto the output + } + // If we've a comma when we're expecting an operand, then what we actually have is a null operand; + // so push a null onto the stack + if (($expectingOperand) || (!$expectingOperator)) { + $output[] = $stack->getStackItem('Empty Argument', null, 'NULL'); + } + // make sure there was a function + $d = $stack->last(2); + /** @var string */ + $temp = $d['value'] ?? ''; + if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $temp, $matches)) { + // Can we inject a dummy function at this point so that the braces at least have some context + // because at least the braces are paired up (at this stage in the formula) + // MS Excel allows this if the content is cell references; but doesn't allow actual values, + // but at this point, we can't differentiate (so allow both) + return $this->raiseFormulaError('Formula Error: Unexpected ,'); + /* The following code may be a better choice, but, with + the other changes for this PR, I can no longer come up + with a test case that gets here + $stack->push('Binary Operator', '∪'); + + ++$index; + $expectingOperator = false; + + continue;*/ + } + + /** @var array $d */ + $d = $stack->pop(); + ++$d['value']; // increment the argument count + + $stack->pushStackItem($d); + $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again + + $expectingOperator = false; + $expectingOperand = true; + ++$index; + } elseif ($opCharacter === '(' && !$expectingOperator) { + // Branch pruning: we go deeper + $this->branchPruner->incrementDepth(); + $stack->push('Brace', '(', null); + ++$index; + } elseif ($isOperandOrFunction && !$expectingOperatorCopy) { + // do we now have a function/variable/number? + $expectingOperator = true; + $expectingOperand = false; + $val = $match[1] ?? ''; //* @phpstan-ignore-line + $length = strlen($val); + + if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { + $val = (string) preg_replace('/\s/u', '', $val); + if (isset($phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function + $valToUpper = strtoupper($val); + } else { + $valToUpper = 'NAME.ERROR('; + } + // here $matches[1] will contain values like "IF" + // and $val "IF(" + + $this->branchPruner->functionCall($valToUpper); + + $stack->push('Function', $valToUpper); + // tests if the function is closed right after opening + $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length)); + if ($ax) { + $stack->push('Operand Count for Function ' . $valToUpper . ')', 0); + $expectingOperator = true; + } else { + $stack->push('Operand Count for Function ' . $valToUpper . ')', 1); + $expectingOperator = false; + } + $stack->push('Brace', '('); + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) { + // Watch for this case-change when modifying to allow cell references in different worksheets... + // Should only be applied to the actual cell column, not the worksheet name + // If the last entry on the stack was a : operator, then we have a cell range reference + $testPrevOp = $stack->last(1); + if ($testPrevOp !== null && $testPrevOp['value'] === ':') { + // If we have a worksheet reference, then we're playing with a 3D reference + if ($matches[2] === '') { + // Otherwise, we 'inherit' the worksheet reference from the start cell reference + // The start of the cell range reference should be the last entry in $output + $rangeStartCellRef = $output[count($output) - 1]['value'] ?? ''; + if ($rangeStartCellRef === ':') { + // Do we have chained range operators? + $rangeStartCellRef = $output[count($output) - 2]['value'] ?? ''; + } + /** @var string $rangeStartCellRef */ + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); + if (array_key_exists(2, $rangeStartMatches)) { + if ($rangeStartMatches[2] > '') { + $val = $rangeStartMatches[2] . '!' . $val; + } + } else { + $val = ExcelError::REF(); + } + } else { + $rangeStartCellRef = $output[count($output) - 1]['value'] ?? ''; + if ($rangeStartCellRef === ':') { + // Do we have chained range operators? + $rangeStartCellRef = $output[count($output) - 2]['value'] ?? ''; + } + /** @var string $rangeStartCellRef */ + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); + if (isset($rangeStartMatches[2]) && $rangeStartMatches[2] !== $matches[2]) { + return $this->raiseFormulaError('3D Range references are not yet supported'); + } + } + } elseif (!str_contains($val, '!') && $pCellParent !== null) { + $worksheet = $pCellParent->getTitle(); + $val = "'{$worksheet}'!{$val}"; + } + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); + $outputItem = $stack->getStackItem('Cell Reference', $val, $val); + + $output[] = $outputItem; + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE . '$/miu', $val, $matches)) { + try { + $structuredReference = Operands\StructuredReference::fromParser($formula, $index, $matches); + } catch (Exception $e) { + return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e); + } + + $val = $structuredReference->value(); + $length = strlen($val); + $outputItem = $stack->getStackItem(Operands\StructuredReference::NAME, $structuredReference, null); + + $output[] = $outputItem; + $expectingOperator = true; + } else { + // it's a variable, constant, string, number or boolean + $localeConstant = false; + $stackItemType = 'Value'; + $stackItemReference = null; + + // If the last entry on the stack was a : operator, then we may have a row or column range reference + $testPrevOp = $stack->last(1); + if ($testPrevOp !== null && $testPrevOp['value'] === ':') { + $stackItemType = 'Cell Reference'; + + if ( + !is_numeric($val) + && ((ctype_alpha($val) === false || strlen($val) > 3)) + && (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false) + && ($this->spreadsheet === null || $this->spreadsheet->getNamedRange($val) !== null) + ) { + $namedRange = ($this->spreadsheet === null) ? null : $this->spreadsheet->getNamedRange($val); + if ($namedRange !== null) { + $stackItemType = 'Defined Name'; + $address = str_replace('$', '', $namedRange->getValue()); + $stackItemReference = $val; + if (str_contains($address, ':')) { + // We'll need to manipulate the stack for an actual named range rather than a named cell + $fromTo = explode(':', $address); + $to = array_pop($fromTo); + foreach ($fromTo as $from) { + $output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference); + $output[] = $stack->getStackItem('Binary Operator', ':'); + } + $address = $to; + } + $val = $address; + } + } elseif ($val === ExcelError::REF()) { + $stackItemReference = $val; + } else { + /** @var non-empty-string $startRowColRef */ + $startRowColRef = $output[count($output) - 1]['value'] ?? ''; + [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true); + $rangeSheetRef = $rangeWS1; + if ($rangeWS1 !== '') { + $rangeWS1 .= '!'; + } + if (str_starts_with($rangeSheetRef, "'")) { + $rangeSheetRef = Worksheet::unApostrophizeTitle($rangeSheetRef); + } + [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); + if ($rangeWS2 !== '') { + $rangeWS2 .= '!'; + } else { + $rangeWS2 = $rangeWS1; + } + + $refSheet = $pCellParent; + if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) { + $refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef); + } + + if (ctype_digit($val) && $val <= 1048576) { + // Row range + $stackItemType = 'Row Reference'; + $valx = $val; + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : AddressRange::MAX_COLUMN; // Max 16,384 columns for Excel2007 + $val = "{$rangeWS2}{$endRowColRef}{$val}"; + } elseif (ctype_alpha($val) && strlen($val) <= 3) { + // Column range + $stackItemType = 'Column Reference'; + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007 + $val = "{$rangeWS2}{$val}{$endRowColRef}"; + } + $stackItemReference = $val; + } + } elseif ($opCharacter === self::FORMULA_STRING_QUOTE) { + // UnEscape any quotes within the string + $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, StringHelper::convertToString(self::unwrapResult($val)))); + } elseif (isset(self::EXCEL_CONSTANTS[trim(strtoupper($val))])) { + $stackItemType = 'Constant'; + $excelConstant = trim(strtoupper($val)); + $val = self::EXCEL_CONSTANTS[$excelConstant]; + $stackItemReference = $excelConstant; + } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { + $stackItemType = 'Constant'; + $val = self::EXCEL_CONSTANTS[$localeConstant]; + $stackItemReference = $localeConstant; + } elseif ( + preg_match('/^' . self::CALCULATION_REGEXP_ROW_RANGE . '/miu', substr($formula, $index), $rowRangeReference) + ) { + $val = $rowRangeReference[1]; + $length = strlen($rowRangeReference[1]); + $stackItemType = 'Row Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); + $column = 'A'; + if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { + $column = $pCellParent->getHighestDataColumn($val); + } + $val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}"; + $stackItemReference = $val; + } elseif ( + preg_match('/^' . self::CALCULATION_REGEXP_COLUMN_RANGE . '/miu', substr($formula, $index), $columnRangeReference) + ) { + $val = $columnRangeReference[1]; + $length = strlen($val); + $stackItemType = 'Column Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); + $row = '1'; + if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { + $row = $pCellParent->getHighestDataRow($val); + } + $val = "{$val}{$row}"; + $stackItemReference = $val; + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', $val, $match)) { + $stackItemType = 'Defined Name'; + $stackItemReference = $val; + } elseif (is_numeric($val)) { + if ((str_contains((string) $val, '.')) || (stripos((string) $val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) { + $val = (float) $val; + } else { + $val = (int) $val; + } + } + + $details = $stack->getStackItem($stackItemType, $val, $stackItemReference); + if ($localeConstant) { + $details['localeValue'] = $localeConstant; + } + $output[] = $details; + } + $index += $length; + } elseif ($opCharacter === '$') { // absolute row or column range + ++$index; + } elseif ($opCharacter === ')') { // miscellaneous error checking + if ($expectingOperand) { + $output[] = $stack->getStackItem('Empty Argument', null, 'NULL'); + $expectingOperand = false; + $expectingOperator = true; + } else { + return $this->raiseFormulaError("Formula Error: Unexpected ')'"); + } + } elseif (isset(self::CALCULATION_OPERATORS[$opCharacter]) && !$expectingOperator) { + return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'"); + } else { // I don't even want to know what you did to get here + return $this->raiseFormulaError('Formula Error: An unexpected error occurred'); + } + // Test for end of formula string + if ($index == strlen($formula)) { + // Did we end with an operator?. + // Only valid for the % unary operator + if ((isset(self::CALCULATION_OPERATORS[$opCharacter])) && ($opCharacter != '%')) { + return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands"); + } + + break; + } + // Ignore white space + while (($formula[$index] === "\n") || ($formula[$index] === "\r")) { + ++$index; + } + + if ($formula[$index] === ' ') { + while ($formula[$index] === ' ') { + ++$index; + } + + // If we're expecting an operator, but only have a space between the previous and next operands (and both are + // Cell References, Defined Names or Structured References) then we have an INTERSECTION operator + $countOutputMinus1 = count($output) - 1; + if ( + ($expectingOperator) + && array_key_exists($countOutputMinus1, $output) + && is_array($output[$countOutputMinus1]) + && array_key_exists('type', $output[$countOutputMinus1]) + && ( + (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/miu', substr($formula, $index), $match)) + && ($output[$countOutputMinus1]['type'] === 'Cell Reference') + || (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match)) + && ($output[$countOutputMinus1]['type'] === 'Defined Name' || $output[$countOutputMinus1]['type'] === 'Value') + || (preg_match('/^' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE . '.*/miu', substr($formula, $index), $match)) + && ($output[$countOutputMinus1]['type'] === Operands\StructuredReference::NAME || $output[$countOutputMinus1]['type'] === 'Value') + ) + ) { + while (self::swapOperands($stack, $opCharacter)) { + $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output + } + $stack->push('Binary Operator', '∩'); // Put an Intersect Operator on the stack + $expectingOperator = false; + } + } + } + + while (($op = $stack->pop()) !== null) { + // pop everything off the stack and push onto output + if ($op['value'] == '(') { + return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced + } + $output[] = $op; + } + + return $output; + } + + /** @param mixed[] $operandData */ + private static function dataTestReference(array &$operandData): mixed + { + $operand = $operandData['value']; + if (($operandData['reference'] === null) && (is_array($operand))) { + $rKeys = array_keys($operand); + $rowKey = array_shift($rKeys); + if (is_array($operand[$rowKey]) === false) { + $operandData['value'] = $operand[$rowKey]; + + return $operand[$rowKey]; + } + + $cKeys = array_keys(array_keys($operand[$rowKey])); + $colKey = array_shift($cKeys); + if (ctype_upper("$colKey")) { + $operandData['reference'] = $colKey . $rowKey; + } + } + + return $operand; + } + + private static int $matchIndex8 = 8; + + private static int $matchIndex9 = 9; + + private static int $matchIndex10 = 10; + + /** + * @param array|false $tokens + * + * @return array|false|string + */ + private function processTokenStack(false|array $tokens, ?string $cellID = null, ?Cell $cell = null) + { + if ($tokens === false) { + return false; + } + $phpSpreadsheetFunctions = &self::getFunctionsAddress(); + + // If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection), + // so we store the parent cell collection so that we can re-attach it when necessary + $pCellWorksheet = ($cell !== null) ? $cell->getWorksheet() : null; + $originalCoordinate = $cell?->getCoordinate(); + $pCellParent = ($cell !== null) ? $cell->getParent() : null; + $stack = new Stack($this->branchPruner); + + // Stores branches that have been pruned + $fakedForBranchPruning = []; + // help us to know when pruning ['branchTestId' => true/false] + $branchStore = []; + // Loop through each token in turn + foreach ($tokens as $tokenIdx => $tokenData) { + /** @var mixed[] $tokenData */ + $this->processingAnchorArray = false; + if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore-line + $this->processingAnchorArray = true; + } + $token = $tokenData['value']; + // Branch pruning: skip useless resolutions + /** @var ?string */ + $storeKey = $tokenData['storeKey'] ?? null; + if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) { + /** @var string */ + $onlyIfStoreKey = $tokenData['onlyIf']; + $storeValue = $branchStore[$onlyIfStoreKey] ?? null; + $storeValueAsBool = ($storeValue === null) + ? true : (bool) Functions::flattenSingleValue($storeValue); + if (is_array($storeValue)) { + $wrappedItem = end($storeValue); + $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem; + } + + if ( + (isset($storeValue) || $tokenData['reference'] === 'NULL') + && (!$storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) + ) { + // If branching value is not true, we don't need to compute + /** @var string $onlyIfStoreKey */ + if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) { + /** @var string $token */ + $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token); + $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true; + } + + if (isset($storeKey)) { + // We are processing an if condition + // We cascade the pruning to the depending branches + $branchStore[$storeKey] = 'Pruned branch'; + $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true; + $fakedForBranchPruning['onlyIf-' . $storeKey] = true; + } + + continue; + } + } + + if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) { + /** @var string */ + $onlyIfNotStoreKey = $tokenData['onlyIfNot']; + $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null; + $storeValueAsBool = ($storeValue === null) + ? true : (bool) Functions::flattenSingleValue($storeValue); + if (is_array($storeValue)) { + $wrappedItem = end($storeValue); + $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem; + } + + if ( + (isset($storeValue) || $tokenData['reference'] === 'NULL') + && ($storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) + ) { + // If branching value is true, we don't need to compute + if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) { + /** @var string $token */ + $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token); + $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true; + } + + if (isset($storeKey)) { + // We are processing an if condition + // We cascade the pruning to the depending branches + $branchStore[$storeKey] = 'Pruned branch'; + $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true; + $fakedForBranchPruning['onlyIf-' . $storeKey] = true; + } + + continue; + } + } + + if ($token instanceof Operands\StructuredReference) { + if ($cell === null) { + return $this->raiseFormulaError('Structured References must exist in a Cell context'); + } + + try { + $cellRange = $token->parse($cell); + if (str_contains($cellRange, ':')) { + $this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell Range %s', $token->value(), $cellRange); + $rangeValue = self::getInstance($cell->getWorksheet()->getParent())->_calculateFormulaValue("={$cellRange}", $cellRange, $cell); + $stack->push('Value', $rangeValue); + $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($rangeValue)); + } else { + $this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell %s', $token->value(), $cellRange); + $cellValue = $cell->getWorksheet()->getCell($cellRange)->getCalculatedValue(false); + $stack->push('Cell Reference', $cellValue, $cellRange); + $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($cellValue)); + } + } catch (Exception $e) { + if ($e->getCode() === Exception::CALCULATION_ENGINE_PUSH_TO_STACK) { + $stack->push('Error', ExcelError::REF(), null); + $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as error value %s', $token->value(), ExcelError::REF()); + } else { + return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e); + } + } + } elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore-line + // if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack + // We must have two operands, error if we don't + $operand2Data = $stack->pop(); + if ($operand2Data === null) { + return $this->raiseFormulaError('Internal error - Operand value missing from stack'); + } + $operand1Data = $stack->pop(); + if ($operand1Data === null) { + return $this->raiseFormulaError('Internal error - Operand value missing from stack'); + } + + $operand1 = self::dataTestReference($operand1Data); + $operand2 = self::dataTestReference($operand2Data); + + // Log what we're doing + if ($token == ':') { + $this->debugLog->writeDebugLog('Evaluating Range %s %s %s', $this->showValue($operand1Data['reference']), $token, $this->showValue($operand2Data['reference'])); + } else { + $this->debugLog->writeDebugLog('Evaluating %s %s %s', $this->showValue($operand1), $token, $this->showValue($operand2)); + } + + // Process the operation in the appropriate manner + switch ($token) { + // Comparison (Boolean) Operators + case '>': // Greater than + case '<': // Less than + case '>=': // Greater than or Equal to + case '<=': // Less than or Equal to + case '=': // Equality + case '<>': // Inequality + $result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + + break; + // Binary Operators + case ':': // Range + if ($operand1Data['type'] === 'Error') { + $stack->push($operand1Data['type'], $operand1Data['value'], null); + + break; + } + if ($operand2Data['type'] === 'Error') { + $stack->push($operand2Data['type'], $operand2Data['value'], null); + + break; + } + if ($operand1Data['type'] === 'Defined Name') { + /** @var array{reference: string} $operand1Data */ + if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false && $this->spreadsheet !== null) { + /** @var string[] $operand1Data */ + $definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']); + if ($definedName !== null) { + $operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue()); + } + } + } + /** @var array{reference?: ?string} $operand1Data */ + if (str_contains($operand1Data['reference'] ?? '', '!')) { + [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true, true); + } else { + $sheet1 = ($pCellWorksheet !== null) ? $pCellWorksheet->getTitle() : ''; + } + //$sheet1 ??= ''; // phpstan level 10 says this is unneeded + + /** @var string */ + $op2ref = $operand2Data['reference']; + [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($op2ref, true, true); + if (empty($sheet2)) { + $sheet2 = $sheet1; + } + + if ($sheet1 === $sheet2) { + /** @var array{reference: ?string, value: string|string[]} $operand1Data */ + if ($operand1Data['reference'] === null && $cell !== null) { + if (is_array($operand1Data['value'])) { + $operand1Data['reference'] = $cell->getCoordinate(); + } elseif ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) { + $operand1Data['reference'] = $cell->getColumn() . $operand1Data['value']; + } elseif (trim($operand1Data['value']) == '') { + $operand1Data['reference'] = $cell->getCoordinate(); + } else { + $operand1Data['reference'] = $operand1Data['value'] . $cell->getRow(); + } + } + /** @var array{reference: ?string, value: string|string[]} $operand2Data */ + if ($operand2Data['reference'] === null && $cell !== null) { + if (is_array($operand2Data['value'])) { + $operand2Data['reference'] = $cell->getCoordinate(); + } elseif ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) { + $operand2Data['reference'] = $cell->getColumn() . $operand2Data['value']; + } elseif (trim($operand2Data['value']) == '') { + $operand2Data['reference'] = $cell->getCoordinate(); + } else { + $operand2Data['reference'] = $operand2Data['value'] . $cell->getRow(); + } + } + + $oData = array_merge(explode(':', $operand1Data['reference'] ?? ''), explode(':', $operand2Data['reference'] ?? '')); + $oCol = $oRow = []; + $breakNeeded = false; + foreach ($oData as $oDatum) { + try { + $oCR = Coordinate::coordinateFromString($oDatum); + $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1; + $oRow[] = $oCR[1]; + } catch (\Exception) { + $stack->push('Error', ExcelError::REF(), null); + $breakNeeded = true; + + break; + } + } + if ($breakNeeded) { + break; + } + $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line + if ($pCellParent !== null && $this->spreadsheet !== null) { + $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false); + } else { + return $this->raiseFormulaError('Unable to access Cell Reference'); + } + + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellValue)); + $stack->push('Cell Reference', $cellValue, $cellRef); + } else { + $this->debugLog->writeDebugLog('Evaluation Result is a #REF! Error'); + $stack->push('Error', ExcelError::REF(), null); + } + + break; + case '+': // Addition + case '-': // Subtraction + case '*': // Multiplication + case '/': // Division + case '^': // Exponential + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + + break; + case '&': // Concatenation + // If either of the operands is a matrix, we need to treat them both as matrices + // (converting the other operand to a matrix if need be); then perform the required + // matrix operation + $operand1 = self::boolToString($operand1); + $operand2 = self::boolToString($operand2); + if (is_array($operand1) || is_array($operand2)) { + if (is_string($operand1)) { + $operand1 = self::unwrapResult($operand1); + } + if (is_string($operand2)) { + $operand2 = self::unwrapResult($operand2); + } + // Ensure that both operands are arrays/matrices + [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2); + + for ($row = 0; $row < $rows; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + /** @var mixed[][] $operand1 */ + $op1x = self::boolToString($operand1[$row][$column]); + /** @var mixed[][] $operand2 */ + $op2x = self::boolToString($operand2[$row][$column]); + if (Information\ErrorValue::isError($op1x)) { + // no need to do anything + } elseif (Information\ErrorValue::isError($op2x)) { + $operand1[$row][$column] = $op2x; + } else { + /** @var string $op1x */ + /** @var string $op2x */ + $operand1[$row][$column] + = StringHelper::substring( + $op1x . $op2x, + 0, + DataType::MAX_STRING_LENGTH + ); + } + } + } + $result = $operand1; + } else { + if (Information\ErrorValue::isError($operand1)) { + $result = $operand1; + } elseif (Information\ErrorValue::isError($operand2)) { + $result = $operand2; + } else { + $result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore-line + $result = StringHelper::substring( + $result, + 0, + DataType::MAX_STRING_LENGTH + ); + $result = self::FORMULA_STRING_QUOTE . $result . self::FORMULA_STRING_QUOTE; + } + } + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); + $stack->push('Value', $result); + + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + + break; + case '∩': // Intersect + /** @var mixed[][] $operand1 */ + /** @var mixed[][] $operand2 */ + $rowIntersect = array_intersect_key($operand1, $operand2); + $cellIntersect = $oCol = $oRow = []; + foreach (array_keys($rowIntersect) as $row) { + $oRow[] = $row; + foreach ($rowIntersect[$row] as $col => $data) { + $oCol[] = Coordinate::columnIndexFromString($col) - 1; + $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]); + } + } + if (count(Functions::flattenArray($cellIntersect)) === 0) { + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect)); + $stack->push('Error', ExcelError::null(), null); + } else { + $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore-line + . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect)); + $stack->push('Value', $cellIntersect, $cellRef); + } + + break; + case '∪': // union + /** @var mixed[][] $operand1 */ + /** @var mixed[][] $operand2 */ + $cellUnion = array_merge($operand1, $operand2); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellUnion)); + $stack->push('Value', $cellUnion, 'A1'); + + break; + } + } elseif (($token === '~') || ($token === '%')) { + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on + if (($arg = $stack->pop()) === null) { + return $this->raiseFormulaError('Internal error - Operand value missing from stack'); + } + $arg = $arg['value']; + if ($token === '~') { + $this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg)); + $multiplier = -1; + } else { + $this->debugLog->writeDebugLog('Evaluating Percentile of %s', $this->showValue($arg)); + $multiplier = 0.01; + } + if (is_array($arg)) { + $operand2 = $multiplier; + $result = $arg; + [$rows, $columns] = self::checkMatrixOperands($result, $operand2, 0); + for ($row = 0; $row < $rows; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + /** @var mixed[][] $result */ + if (self::isNumericOrBool($result[$row][$column])) { + /** @var float|int|numeric-string */ + $temp = $result[$row][$column]; + $result[$row][$column] = $temp * $multiplier; + } else { + $result[$row][$column] = self::makeError($result[$row][$column]); + } + } + } + + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); + $stack->push('Value', $result); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + } else { + $this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack); + } + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) { + $cellRef = null; + + /* Phpstan says matches[8/9/10] is never set, + and code coverage report seems to confirm. + Appease PhpStan for now; + probably delete this block later. + */ + if (isset($matches[self::$matchIndex8])) { + if ($cell === null) { + // We can't access the range, so return a REF error + $cellValue = ExcelError::REF(); + } else { + $cellRef = $matches[6] . $matches[7] . ':' . $matches[self::$matchIndex9] . $matches[self::$matchIndex10]; + if ($matches[2] > '') { + $matches[2] = trim($matches[2], "\"'"); + if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) { + // It's a Reference to an external spreadsheet (not currently supported) + return $this->raiseFormulaError('Unable to access External Workbook'); + } + $matches[2] = trim($matches[2], "\"'"); + $this->debugLog->writeDebugLog('Evaluating Cell Range %s in worksheet %s', $cellRef, $matches[2]); + if ($pCellParent !== null && $this->spreadsheet !== null) { + $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false); + } else { + return $this->raiseFormulaError('Unable to access Cell Reference'); + } + $this->debugLog->writeDebugLog('Evaluation Result for cells %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue)); + } else { + $this->debugLog->writeDebugLog('Evaluating Cell Range %s in current worksheet', $cellRef); + if ($pCellParent !== null) { + $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false); + } else { + return $this->raiseFormulaError('Unable to access Cell Reference'); + } + $this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue)); + } + } + } else { + if ($cell === null) { + // We can't access the cell, so return a REF error + $cellValue = ExcelError::REF(); + } else { + $cellRef = $matches[6] . $matches[7]; + if ($matches[2] > '') { + $matches[2] = trim($matches[2], "\"'"); + if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) { + // It's a Reference to an external spreadsheet (not currently supported) + return $this->raiseFormulaError('Unable to access External Workbook'); + } + $this->debugLog->writeDebugLog('Evaluating Cell %s in worksheet %s', $cellRef, $matches[2]); + if ($pCellParent !== null && $this->spreadsheet !== null) { + $cellSheet = $this->spreadsheet->getSheetByName($matches[2]); + if ($cellSheet && !$cellSheet->cellExists($cellRef)) { + $cellSheet->setCellValue($cellRef, null); + } + if ($cellSheet && $cellSheet->cellExists($cellRef)) { + $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false); + $cell->attach($pCellParent); + } else { + $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef; + $cellValue = ($cellSheet !== null) ? null : ExcelError::REF(); + } + } else { + return $this->raiseFormulaError('Unable to access Cell Reference'); + } + $this->debugLog->writeDebugLog('Evaluation Result for cell %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue)); + } else { + $this->debugLog->writeDebugLog('Evaluating Cell %s in current worksheet', $cellRef); + if ($pCellParent !== null && $pCellParent->has($cellRef)) { + $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false); + $cell->attach($pCellParent); + } else { + $cellValue = null; + } + $this->debugLog->writeDebugLog('Evaluation Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue)); + } + } + } + + if ($this->getInstanceArrayReturnType() === self::RETURN_ARRAY_AS_ARRAY && !$this->processingAnchorArray && is_array($cellValue)) { + while (is_array($cellValue)) { + $cellValue = array_shift($cellValue); + } + if (is_string($cellValue)) { + $cellValue = preg_replace('/"/', '""', $cellValue); + } + $this->debugLog->writeDebugLog('Scalar Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue)); + } + $this->processingAnchorArray = false; + $stack->push('Cell Value', $cellValue, $cellRef); + if (isset($storeKey)) { + $branchStore[$storeKey] = $cellValue; + } + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', StringHelper::convertToString($token ?? ''), $matches)) { + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on + if ($cell !== null && $pCellParent !== null) { + $cell->attach($pCellParent); + } + + $functionName = $matches[1]; + /** @var array $argCount */ + $argCount = $stack->pop(); + $argCount = $argCount['value']; + if ($functionName !== 'MKMATRIX') { + $this->debugLog->writeDebugLog('Evaluating Function %s() with %s argument%s', self::localeFunc($functionName), (($argCount == 0) ? 'no' : $argCount), (($argCount == 1) ? '' : 's')); + } + if ((isset($phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function + $passByReference = false; + $passCellReference = false; + $functionCall = null; + if (isset($phpSpreadsheetFunctions[$functionName])) { + $functionCall = $phpSpreadsheetFunctions[$functionName]['functionCall']; + $passByReference = isset($phpSpreadsheetFunctions[$functionName]['passByReference']); + $passCellReference = isset($phpSpreadsheetFunctions[$functionName]['passCellReference']); + } elseif (isset(self::$controlFunctions[$functionName])) { + $functionCall = self::$controlFunctions[$functionName]['functionCall']; + $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']); + $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']); + } + + // get the arguments for this function + $args = $argArrayVals = []; + $emptyArguments = []; + for ($i = 0; $i < $argCount; ++$i) { + $arg = $stack->pop(); + $a = $argCount - $i - 1; + if ( + ($passByReference) + && (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore-line + && ($phpSpreadsheetFunctions[$functionName]['passByReference'][$a]) + ) { + /** @var mixed[] $arg */ + if ($arg['reference'] === null) { + $nextArg = $cellID; + if ($functionName === 'ISREF' && ($arg['type'] ?? '') === 'Value') { + if (array_key_exists('value', $arg)) { + $argValue = $arg['value']; + if (is_scalar($argValue)) { + $nextArg = $argValue; + } elseif (empty($argValue)) { + $nextArg = ''; + } + } + } elseif (($arg['type'] ?? '') === 'Error') { + $argValue = $arg['value']; + if (is_scalar($argValue)) { + $nextArg = $argValue; + } elseif (empty($argValue)) { + $nextArg = ''; + } + } + $args[] = $nextArg; + if ($functionName !== 'MKMATRIX') { + $argArrayVals[] = $this->showValue($cellID); + } + } else { + $args[] = $arg['reference']; + if ($functionName !== 'MKMATRIX') { + $argArrayVals[] = $this->showValue($arg['reference']); + } + } + } else { + /** @var mixed[] $arg */ + if ($arg['type'] === 'Empty Argument' && in_array($functionName, ['MIN', 'MINA', 'MAX', 'MAXA', 'IF'], true)) { + $emptyArguments[] = false; + $args[] = $arg['value'] = 0; + $this->debugLog->writeDebugLog('Empty Argument reevaluated as 0'); + } else { + $emptyArguments[] = $arg['type'] === 'Empty Argument'; + $args[] = self::unwrapResult($arg['value']); + } + if ($functionName !== 'MKMATRIX') { + $argArrayVals[] = $this->showValue($arg['value']); + } + } + } + + // Reverse the order of the arguments + krsort($args); + krsort($emptyArguments); + + if ($argCount > 0 && is_array($functionCall)) { + /** @var string[] */ + $functionCallCopy = $functionCall; + $args = $this->addDefaultArgumentValues($functionCallCopy, $args, $emptyArguments); + } + + if (($passByReference) && ($argCount == 0)) { + $args[] = $cellID; + $argArrayVals[] = $this->showValue($cellID); + } + + if ($functionName !== 'MKMATRIX') { + if ($this->debugLog->getWriteDebugLog()) { + krsort($argArrayVals); + $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); + } + } + + // Process the argument with the appropriate function call + if ($pCellWorksheet !== null && $originalCoordinate !== null) { + $pCellWorksheet->getCell($originalCoordinate); + } + /** @var array|string $functionCall */ + $args = $this->addCellReference($args, $passCellReference, $functionCall, $cell); + + if (!is_array($functionCall)) { + foreach ($args as &$arg) { + $arg = Functions::flattenSingleValue($arg); + } + unset($arg); + } + + /** @var callable $functionCall */ + try { + $result = call_user_func_array($functionCall, $args); + } catch (TypeError $e) { + if (!$this->suppressFormulaErrors) { + throw $e; + } + $result = false; + } + if ($functionName !== 'MKMATRIX') { + $this->debugLog->writeDebugLog('Evaluation Result for %s() function call is %s', self::localeFunc($functionName), $this->showTypeDetails($result)); + } + $stack->push('Value', self::wrapResult($result)); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + } + } else { + // if the token is a number, boolean, string or an Excel error, push it onto the stack + /** @var ?string $token */ + if (isset(self::EXCEL_CONSTANTS[strtoupper($token ?? '')])) { + $excelConstant = strtoupper("$token"); + $stack->push('Constant Value', self::EXCEL_CONSTANTS[$excelConstant]); + if (isset($storeKey)) { + $branchStore[$storeKey] = self::EXCEL_CONSTANTS[$excelConstant]; + } + $this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::EXCEL_CONSTANTS[$excelConstant])); + } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore-line + /** @var array{type: string, reference: ?string} $tokenData */ + $stack->push($tokenData['type'], $token, $tokenData['reference']); + if (isset($storeKey)) { + $branchStore[$storeKey] = $token; + } + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) { + // if the token is a named range or formula, evaluate it and push the result onto the stack + $definedName = $matches[6]; + if (str_starts_with($definedName, '_xleta')) { + return Functions::NOT_YET_IMPLEMENTED; + } + if ($cell === null || $pCellWorksheet === null) { + return $this->raiseFormulaError("undefined name '$token'"); + } + $specifiedWorksheet = trim($matches[2], "'"); + + $this->debugLog->writeDebugLog('Evaluating Defined Name %s', $definedName); + $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet, $specifiedWorksheet); + // If not Defined Name, try as Table. + if ($namedRange === null && $this->spreadsheet !== null) { + $table = $this->spreadsheet->getTableByName($definedName); + if ($table !== null) { + $tableRange = Coordinate::getRangeBoundaries($table->getRange()); + if ($table->getShowHeaderRow()) { + ++$tableRange[0][1]; + } + if ($table->getShowTotalsRow()) { + --$tableRange[1][1]; + } + $tableRangeString + = '$' . $tableRange[0][0] + . '$' . $tableRange[0][1] + . ':' + . '$' . $tableRange[1][0] + . '$' . $tableRange[1][1]; + $namedRange = new NamedRange($definedName, $table->getWorksheet(), $tableRangeString); + } + } + if ($namedRange === null) { + $result = ExcelError::NAME(); + $stack->push('Error', $result, null); + $this->debugLog->writeDebugLog("Error $result"); + } else { + $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== ''); + } + + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + } else { + return $this->raiseFormulaError("undefined name '$token'"); + } + } + } + // when we're out of tokens, the stack should have a single element, the final result + if ($stack->count() != 1) { + return $this->raiseFormulaError('internal error'); + } + /** @var array|false|string> */ + $output = $stack->pop(); + $output = $output['value']; + + return $output; + } + + private function validateBinaryOperand(mixed &$operand, Stack &$stack): bool + { + if (is_array($operand)) { + if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) { + do { + $operand = array_pop($operand); + } while (is_array($operand)); + } + } + // Numbers, matrices and booleans can pass straight through, as they're already valid + if (is_string($operand)) { + // We only need special validations for the operand if it is a string + // Start by stripping off the quotation marks we use to identify true excel string values internally + if ($operand > '' && $operand[0] == self::FORMULA_STRING_QUOTE) { + $operand = StringHelper::convertToString(self::unwrapResult($operand)); + } + // If the string is a numeric value, we treat it as a numeric, so no further testing + if (!is_numeric($operand)) { + // If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations + if ($operand > '' && $operand[0] == '#') { + $stack->push('Value', $operand); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($operand)); + + return false; + } elseif (Engine\FormattedNumber::convertToNumberIfFormatted($operand) === false) { + // If not a numeric, a fraction or a percentage, then it's a text string, and so can't be used in mathematical binary operations + $stack->push('Error', '#VALUE!'); + $this->debugLog->writeDebugLog('Evaluation Result is a %s', $this->showTypeDetails('#VALUE!')); + + return false; + } + } + } + + // return a true if the value of the operand is one that we can use in normal binary mathematical operations + return true; + } + + /** @return mixed[] */ + private function executeArrayComparison(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays): array + { + $result = []; + if (!is_array($operand2) && is_array($operand1)) { + // Operand 1 is an array, Operand 2 is a scalar + foreach ($operand1 as $x => $operandData) { + $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2)); + $this->executeBinaryComparisonOperation($operandData, $operand2, $operation, $stack); + /** @var array $r */ + $r = $stack->pop(); + $result[$x] = $r['value']; + } + } elseif (is_array($operand2) && !is_array($operand1)) { + // Operand 1 is a scalar, Operand 2 is an array + foreach ($operand2 as $x => $operandData) { + $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operand1), $operation, $this->showValue($operandData)); + $this->executeBinaryComparisonOperation($operand1, $operandData, $operation, $stack); + /** @var array $r */ + $r = $stack->pop(); + $result[$x] = $r['value']; + } + } elseif (is_array($operand2) && is_array($operand1)) { + // Operand 1 and Operand 2 are both arrays + if (!$recursingArrays) { + self::checkMatrixOperands($operand1, $operand2, 2); + } + foreach ($operand1 as $x => $operandData) { + $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2[$x])); + $this->executeBinaryComparisonOperation($operandData, $operand2[$x], $operation, $stack, true); + /** @var array $r */ + $r = $stack->pop(); + $result[$x] = $r['value']; + } + } else { + throw new Exception('Neither operand is an arra'); + } + // Log the result details + $this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result)); + // And push the result onto the stack + $stack->push('Array', $result); + + return $result; + } + + /** @return bool|mixed[] */ + private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool + { + // If we're dealing with matrix operations, we want a matrix result + if ((is_array($operand1)) || (is_array($operand2))) { + return $this->executeArrayComparison($operand1, $operand2, $operation, $stack, $recursingArrays); + } + + $result = BinaryComparison::compare($operand1, $operand2, $operation); + + // Log the result details + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); + // And push the result onto the stack + $stack->push('Value', $result); + + return $result; + } + + private function executeNumericBinaryOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack): mixed + { + // Validate the two operands + if ( + ($this->validateBinaryOperand($operand1, $stack) === false) + || ($this->validateBinaryOperand($operand2, $stack) === false) + ) { + return false; + } + + if ( + (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) + && ((is_string($operand1) && !is_numeric($operand1) && $operand1 !== '') + || (is_string($operand2) && !is_numeric($operand2) && $operand2 !== '')) + ) { + $result = ExcelError::VALUE(); + } elseif (is_array($operand1) || is_array($operand2)) { + // Ensure that both operands are arrays/matrices + if (is_array($operand1)) { + foreach ($operand1 as $key => $value) { + $operand1[$key] = Functions::flattenArray($value); + } + } + if (is_array($operand2)) { + foreach ($operand2 as $key => $value) { + $operand2[$key] = Functions::flattenArray($value); + } + } + [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 3); + + for ($row = 0; $row < $rows; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + /** @var mixed[][] $operand1 */ + if (($operand1[$row][$column] ?? null) === null) { + $operand1[$row][$column] = 0; + } elseif (!self::isNumericOrBool($operand1[$row][$column])) { + $operand1[$row][$column] = self::makeError($operand1[$row][$column]); + + continue; + } + /** @var mixed[][] $operand2 */ + if (($operand2[$row][$column] ?? null) === null) { + $operand2[$row][$column] = 0; + } elseif (!self::isNumericOrBool($operand2[$row][$column])) { + $operand1[$row][$column] = self::makeError($operand2[$row][$column]); + + continue; + } + /** @var float|int */ + $operand1Val = $operand1[$row][$column]; + /** @var float|int */ + $operand2Val = $operand2[$row][$column]; + switch ($operation) { + case '+': + $operand1[$row][$column] = $operand1Val + $operand2Val; + + break; + case '-': + $operand1[$row][$column] = $operand1Val - $operand2Val; + + break; + case '*': + $operand1[$row][$column] = $operand1Val * $operand2Val; + + break; + case '/': + if ($operand2Val == 0) { + $operand1[$row][$column] = ExcelError::DIV0(); + } else { + $operand1[$row][$column] = $operand1Val / $operand2Val; + } + + break; + case '^': + $operand1[$row][$column] = $operand1Val ** $operand2Val; + + break; + + default: + throw new Exception('Unsupported numeric binary operation'); + } + } + } + $result = $operand1; + } else { + // If we're dealing with non-matrix operations, execute the necessary operation + /** @var float|int $operand1 */ + /** @var float|int $operand2 */ + switch ($operation) { + // Addition + case '+': + $result = $operand1 + $operand2; + + break; + // Subtraction + case '-': + $result = $operand1 - $operand2; + + break; + // Multiplication + case '*': + $result = $operand1 * $operand2; + + break; + // Division + case '/': + if ($operand2 == 0) { + // Trap for Divide by Zero error + $stack->push('Error', ExcelError::DIV0()); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails(ExcelError::DIV0())); + + return false; + } + $result = $operand1 / $operand2; + + break; + // Power + case '^': + $result = $operand1 ** $operand2; + + break; + + default: + throw new Exception('Unsupported numeric binary operation'); + } + } + + // Log the result details + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); + // And push the result onto the stack + $stack->push('Value', $result); + + return $result; + } + + /** + * Trigger an error, but nicely, if need be. + * + * @return false + */ + protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throwable $exception = null): bool + { + $this->formulaError = $errorMessage; + $this->cyclicReferenceStack->clear(); + $suppress = $this->suppressFormulaErrors; + $suppressed = $suppress ? ' $suppressed' : ''; + $this->debugLog->writeDebugLog("Raise Error$suppressed $errorMessage"); + if (!$suppress) { + throw new Exception($errorMessage, $code, $exception); + } + + return false; + } + + /** + * Extract range values. + * + * @param string $range String based range representation + * @param ?Worksheet $worksheet Worksheet + * @param bool $resetLog Flag indicating whether calculation log should be reset or not + * + * @return mixed[] Array of values in range if range contains more than one element. Otherwise, a single value is returned. + */ + public function extractCellRange(string &$range = 'A1', ?Worksheet $worksheet = null, bool $resetLog = true, bool $createCell = false): array + { + // Return value + /** @var mixed[][] */ + $returnValue = []; + + if ($worksheet !== null) { + $worksheetName = $worksheet->getTitle(); + + if (str_contains($range, '!')) { + [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true, true); + $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName); + } + + // Extract range + $aReferences = Coordinate::extractAllCellReferencesInRange($range); + $range = "'" . $worksheetName . "'" . '!' . $range; + $currentCol = ''; + $currentRow = 0; + if (!isset($aReferences[1])) { + // Single cell in range + sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow); + /** @var string $currentCol */ + /** @var int $currentRow */ + if ($createCell && $worksheet !== null && !$worksheet->cellExists($aReferences[0])) { + $worksheet->setCellValue($aReferences[0], null); + } + if ($worksheet !== null && $worksheet->cellExists($aReferences[0])) { + $temp = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog); + if ($this->getInstanceArrayReturnType() === self::RETURN_ARRAY_AS_ARRAY) { + while (is_array($temp)) { + $temp = array_shift($temp); + } + } + $returnValue[$currentRow][$currentCol] = $temp; + } else { + $returnValue[$currentRow][$currentCol] = null; + } + } else { + // Extract cell data for all cells in the range + foreach ($aReferences as $reference) { + // Extract range + sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow); + /** @var string $currentCol */ + /** @var int $currentRow */ + if ($createCell && $worksheet !== null && !$worksheet->cellExists($reference)) { + $worksheet->setCellValue($reference, null); + } + if ($worksheet !== null && $worksheet->cellExists($reference)) { + $temp = $worksheet->getCell($reference)->getCalculatedValue($resetLog); + if ($this->getInstanceArrayReturnType() === self::RETURN_ARRAY_AS_ARRAY) { + while (is_array($temp)) { + $temp = array_shift($temp); + } + } + $returnValue[$currentRow][$currentCol] = $temp; + } else { + $returnValue[$currentRow][$currentCol] = null; + } + } + } + } + + return $returnValue; + } + + /** + * Extract range values. + * + * @param string $range String based range representation + * @param null|Worksheet $worksheet Worksheet + * @param bool $resetLog Flag indicating whether calculation log should be reset or not + * + * @return mixed[]|string Array of values in range if range contains more than one element. Otherwise, a single value is returned. + */ + public function extractNamedRange(string &$range = 'A1', ?Worksheet $worksheet = null, bool $resetLog = true): string|array + { + // Return value + $returnValue = []; + + if ($worksheet !== null) { + if (str_contains($range, '!')) { + [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true, true); + $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName); + } + + // Named range? + $namedRange = ($worksheet === null) ? null : DefinedName::resolveName($range, $worksheet); + if ($namedRange === null) { + return ExcelError::REF(); + } + + $worksheet = $namedRange->getWorksheet(); + $range = $namedRange->getValue(); + $splitRange = Coordinate::splitRange($range); + // Convert row and column references + if ($worksheet !== null && ctype_alpha($splitRange[0][0])) { + $range = $splitRange[0][0] . '1:' . $splitRange[0][1] . $worksheet->getHighestRow(); + } elseif ($worksheet !== null && ctype_digit($splitRange[0][0])) { + $range = 'A' . $splitRange[0][0] . ':' . $worksheet->getHighestColumn() . $splitRange[0][1]; + } + + // Extract range + $aReferences = Coordinate::extractAllCellReferencesInRange($range); + if (!isset($aReferences[1])) { + // Single cell (or single column or row) in range + [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]); + /** @var mixed[][] $returnValue */ + if ($worksheet !== null && $worksheet->cellExists($aReferences[0])) { + $returnValue[$currentRow][$currentCol] = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog); + } else { + $returnValue[$currentRow][$currentCol] = null; + } + } else { + // Extract cell data for all cells in the range + foreach ($aReferences as $reference) { + // Extract range + [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference); + if ($worksheet !== null && $worksheet->cellExists($reference)) { + $returnValue[$currentRow][$currentCol] = $worksheet->getCell($reference)->getCalculatedValue($resetLog); + } else { + $returnValue[$currentRow][$currentCol] = null; + } + } + } + } + + return $returnValue; + } + + /** + * Is a specific function implemented? + * + * @param string $function Function Name + */ + public function isImplemented(string $function): bool + { + $function = strtoupper($function); + $phpSpreadsheetFunctions = &self::getFunctionsAddress(); + $notImplemented = !isset($phpSpreadsheetFunctions[$function]) || (is_array($phpSpreadsheetFunctions[$function]['functionCall']) && $phpSpreadsheetFunctions[$function]['functionCall'][1] === 'DUMMY'); + + return !$notImplemented; + } + + /** + * Get a list of implemented Excel function names. + * + * @return string[] + */ + public function getImplementedFunctionNames(): array + { + $returnValue = []; + $phpSpreadsheetFunctions = &self::getFunctionsAddress(); + foreach ($phpSpreadsheetFunctions as $functionName => $function) { + if ($this->isImplemented($functionName)) { + $returnValue[] = $functionName; + } + } + + return $returnValue; + } + + /** + * @param string[] $functionCall + * @param mixed[] $args + * @param mixed[] $emptyArguments + * + * @return mixed[] + */ + private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array + { + $reflector = new ReflectionMethod($functionCall[0], $functionCall[1]); + $methodArguments = $reflector->getParameters(); + + if (count($methodArguments) > 0) { + // Apply any defaults for empty argument values + foreach ($emptyArguments as $argumentId => $isArgumentEmpty) { + if ($isArgumentEmpty === true) { + $reflectedArgumentId = count($args) - (int) $argumentId - 1; + if ( + !array_key_exists($reflectedArgumentId, $methodArguments) + || $methodArguments[$reflectedArgumentId]->isVariadic() + ) { + break; + } + + $args[$argumentId] = $this->getArgumentDefaultValue($methodArguments[$reflectedArgumentId]); + } + } + } + + return $args; + } + + private function getArgumentDefaultValue(ReflectionParameter $methodArgument): mixed + { + $defaultValue = null; + + if ($methodArgument->isDefaultValueAvailable()) { + $defaultValue = $methodArgument->getDefaultValue(); + if ($methodArgument->isDefaultValueConstant()) { + $constantName = $methodArgument->getDefaultValueConstantName() ?? ''; + // read constant value + if (str_contains($constantName, '::')) { + [$className, $constantName] = explode('::', $constantName); + /** @var class-string $className */ + $constantReflector = new ReflectionClassConstant($className, $constantName); + + return $constantReflector->getValue(); + } + + return constant($constantName); + } + } + + return $defaultValue; + } + + /** + * Add cell reference if needed while making sure that it is the last argument. + * + * @param mixed[] $args + * @param string|string[] $functionCall + * + * @return mixed[] + */ + private function addCellReference(array $args, bool $passCellReference, array|string $functionCall, ?Cell $cell = null): array + { + if ($passCellReference) { + if (is_array($functionCall)) { + $className = $functionCall[0]; + $methodName = $functionCall[1]; + + $reflectionMethod = new ReflectionMethod($className, $methodName); + $argumentCount = count($reflectionMethod->getParameters()); + while (count($args) < $argumentCount - 1) { + $args[] = null; + } + } + + $args[] = $cell; + } + + return $args; + } + + private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksheet $cellWorksheet, Stack $stack, bool $ignoreScope = false): mixed + { + $definedNameScope = $namedRange->getScope(); + if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet && !$ignoreScope) { + // The defined name isn't in our current scope, so #REF + $result = ExcelError::REF(); + $stack->push('Error', $result, $namedRange->getName()); + + return $result; + } + + $definedNameValue = $namedRange->getValue(); + $definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range'; + if ($definedNameType === 'Range') { + if (preg_match('/^(.*!)?(.*)$/', $definedNameValue, $matches) === 1) { + $matches2 = trim($matches[2]); + $matches2 = preg_replace('/ +/', ' ∩ ', $matches2) ?? $matches2; + $matches2 = preg_replace('/,/', ' ∪ ', $matches2) ?? $matches2; + $definedNameValue = $matches[1] . $matches2; + } + } + $definedNameWorksheet = $namedRange->getWorksheet(); + + if ($definedNameValue[0] !== '=') { + $definedNameValue = '=' . $definedNameValue; + } + + $this->debugLog->writeDebugLog('Defined Name is a %s with a value of %s', $definedNameType, $definedNameValue); + + $originalCoordinate = $cell->getCoordinate(); + $recursiveCalculationCell = ($definedNameType !== 'Formula' && $definedNameWorksheet !== null && $definedNameWorksheet !== $cellWorksheet) + ? $definedNameWorksheet->getCell('A1') + : $cell; + $recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate(); + + // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns + $definedNameValue = ReferenceHelper::getInstance() + ->updateFormulaReferencesAnyWorksheet( + $definedNameValue, + Coordinate::columnIndexFromString( + $cell->getColumn() + ) - 1, + $cell->getRow() - 1 + ); + + $this->debugLog->writeDebugLog('Value adjusted for relative references is %s', $definedNameValue); + + $recursiveCalculator = new self($this->spreadsheet); + $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog()); + $recursiveCalculator->getDebugLog()->setEchoDebugLog($this->getDebugLog()->getEchoDebugLog()); + $result = $recursiveCalculator->_calculateFormulaValue($definedNameValue, $recursiveCalculationCellAddress, $recursiveCalculationCell, true); + $cellWorksheet->getCell($originalCoordinate); + + if ($this->getDebugLog()->getWriteDebugLog()) { + $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3)); + $this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result)); + } + + $y = $namedRange->getWorksheet()?->getTitle(); + $x = $namedRange->getLocalOnly(); + if ($x && $y !== null) { + $stack->push('Defined Name', $result, "'$y'!" . $namedRange->getName()); + } else { + $stack->push('Defined Name', $result, $namedRange->getName()); + } + + return $result; + } + + public function setSuppressFormulaErrors(bool $suppressFormulaErrors): self + { + $this->suppressFormulaErrors = $suppressFormulaErrors; + + return $this; + } + + public function getSuppressFormulaErrors(): bool + { + return $this->suppressFormulaErrors; + } + + public static function boolToString(mixed $operand1): mixed + { + if (is_bool($operand1)) { + $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE']; + } elseif ($operand1 === null) { + $operand1 = ''; + } + + return $operand1; + } + + private static function isNumericOrBool(mixed $operand): bool + { + return is_numeric($operand) || is_bool($operand); + } + + private static function makeError(mixed $operand = ''): string + { + return (is_string($operand) && Information\ErrorValue::isError($operand)) ? $operand : ExcelError::VALUE(); + } + + private static function swapOperands(Stack $stack, string $opCharacter): bool + { + $retVal = false; + if ($stack->count() > 0) { + $o2 = $stack->last(); + if ($o2) { + /** @var array{value: string} $o2 */ + if (isset(self::CALCULATION_OPERATORS[$o2['value']])) { + $retVal = (self::OPERATOR_PRECEDENCE[$opCharacter] ?? 0) <= self::OPERATOR_PRECEDENCE[$o2['value']]; + } + } + } + + return $retVal; + } + + public function getSpreadsheet(): ?Spreadsheet + { + return $this->spreadsheet; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/CalculationBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/CalculationBase.php new file mode 100644 index 0000000..5bef04d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/CalculationBase.php @@ -0,0 +1,72 @@ + + */ + public static function getFunctions(): array + { + return FunctionArray::$phpSpreadsheetFunctions; + } + + /** + * Get address of list of all implemented functions as an array of function objects. + * + * @return array> + */ + protected static function &getFunctionsAddress(): array + { + return FunctionArray::$phpSpreadsheetFunctions; + } + + /** + * @param array{category: string, functionCall: string|string[], argumentCount: string, passCellReference?: bool, passByReference?: bool[], custom?: bool} $value + */ + public static function addFunction(string $key, array $value): bool + { + $key = strtoupper($key); + if ( + array_key_exists($key, FunctionArray::$phpSpreadsheetFunctions) + && !self::isDummy($key) + ) { + return false; + } + $value['custom'] = true; + FunctionArray::$phpSpreadsheetFunctions[$key] = $value; + + return true; + } + + private static function isDummy(string $key): bool + { + // key is already known to exist + $functionCall = FunctionArray::$phpSpreadsheetFunctions[$key]['functionCall'] ?? null; + if (!is_array($functionCall)) { + return false; + } + if (($functionCall[1] ?? '') !== 'DUMMY') { + return false; + } + + return true; + } + + public static function removeFunction(string $key): bool + { + $key = strtoupper($key); + if (array_key_exists($key, FunctionArray::$phpSpreadsheetFunctions)) { + if (FunctionArray::$phpSpreadsheetFunctions[$key]['custom'] ?? false) { + unset(FunctionArray::$phpSpreadsheetFunctions[$key]); + + return true; + } + } + + return false; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/CalculationLocale.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/CalculationLocale.php new file mode 100644 index 0000000..40092b6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/CalculationLocale.php @@ -0,0 +1,429 @@ + + */ + protected static array $localeBoolean = [ + 'TRUE' => 'TRUE', + 'FALSE' => 'FALSE', + 'NULL' => 'NULL', + ]; + + /** @var array> */ + protected static array $falseTrueArray = []; + + public static function getLocaleBoolean(string $index): string + { + return self::$localeBoolean[$index]; + } + + protected static function loadLocales(): void + { + $localeFileDirectory = __DIR__ . '/locale/'; + $localeFileNames = glob($localeFileDirectory . '*', GLOB_ONLYDIR) ?: []; + foreach ($localeFileNames as $filename) { + $filename = substr($filename, strlen($localeFileDirectory)); + if ($filename != 'en') { + self::$validLocaleLanguages[] = $filename; + $subdirs = glob("$localeFileDirectory$filename/*", GLOB_ONLYDIR) ?: []; + foreach ($subdirs as $subdir) { + $subdirx = basename($subdir); + self::$validLocaleLanguages[] = "{$filename}_{$subdirx}"; + } + } + } + } + + /** + * Return the locale-specific translation of TRUE. + * + * @return string locale-specific translation of TRUE + */ + public static function getTRUE(): string + { + return self::$localeBoolean['TRUE']; + } + + /** + * Return the locale-specific translation of FALSE. + * + * @return string locale-specific translation of FALSE + */ + public static function getFALSE(): string + { + return self::$localeBoolean['FALSE']; + } + + /** + * Get the currently defined locale code. + */ + public function getLocale(): string + { + return self::$localeLanguage; + } + + protected function getLocaleFile(string $localeDir, string $locale, string $language, string $file): string + { + $localeFileName = $localeDir . str_replace('_', DIRECTORY_SEPARATOR, $locale) + . DIRECTORY_SEPARATOR . $file; + if (!file_exists($localeFileName)) { + // If there isn't a locale specific file, look for a language specific file + $localeFileName = $localeDir . $language . DIRECTORY_SEPARATOR . $file; + if (!file_exists($localeFileName)) { + throw new Exception('Locale file not found'); + } + } + + return $localeFileName; + } + + /** @return array> */ + public function getFalseTrueArray(): array + { + if (!empty(self::$falseTrueArray)) { + return self::$falseTrueArray; + } + if (count(self::$validLocaleLanguages) == 1) { + self::loadLocales(); + } + $falseTrueArray = [['FALSE'], ['TRUE']]; + foreach (self::$validLocaleLanguages as $language) { + if (str_starts_with($language, 'en')) { + continue; + } + $locale = $language; + if (str_contains($locale, '_')) { + [$language] = explode('_', $locale); + } + $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]); + + try { + $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions'); + } catch (Exception $e) { + continue; + } + // Retrieve the list of locale or language specific function names + $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + foreach ($localeFunctions as $localeFunction) { + [$localeFunction] = explode('##', $localeFunction); // Strip out comments + if (str_contains($localeFunction, '=')) { + [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); + if ($fName === 'FALSE') { + $falseTrueArray[0][] = $lfName; + } elseif ($fName === 'TRUE') { + $falseTrueArray[1][] = $lfName; + } + } + } + } + self::$falseTrueArray = $falseTrueArray; + + return $falseTrueArray; + } + + /** + * Set the locale code. + * + * @param string $locale The locale to use for formula translation, eg: 'en_us' + */ + public function setLocale(string $locale): bool + { + // Identify our locale and language + $language = $locale = strtolower($locale); + if (str_contains($locale, '_')) { + [$language] = explode('_', $locale); + } + if (count(self::$validLocaleLanguages) == 1) { + self::loadLocales(); + } + + // Test whether we have any language data for this language (any locale) + if (in_array($language, self::$validLocaleLanguages, true)) { + // initialise language/locale settings + self::$localeFunctions = []; + self::$localeArgumentSeparator = ','; + self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL']; + + // Default is US English, if user isn't requesting US english, then read the necessary data from the locale files + if ($locale !== 'en_us') { + $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]); + + // Search for a file with a list of function names for locale + try { + $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions'); + } catch (Exception $e) { + return false; + } + + // Retrieve the list of locale or language specific function names + $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + $phpSpreadsheetFunctions = &self::getFunctionsAddress(); + foreach ($localeFunctions as $localeFunction) { + [$localeFunction] = explode('##', $localeFunction); // Strip out comments + if (str_contains($localeFunction, '=')) { + [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); + if ((str_starts_with($fName, '*') || isset($phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { + self::$localeFunctions[$fName] = $lfName; + } + } + } + // Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions + if (isset(self::$localeFunctions['TRUE'])) { + self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE']; + } + if (isset(self::$localeFunctions['FALSE'])) { + self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE']; + } + + try { + $configFile = $this->getLocaleFile($localeDir, $locale, $language, 'config'); + } catch (Exception) { + return false; + } + + $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + foreach ($localeSettings as $localeSetting) { + [$localeSetting] = explode('##', $localeSetting); // Strip out comments + if (str_contains($localeSetting, '=')) { + [$settingName, $settingValue] = array_map('trim', explode('=', $localeSetting)); + $settingName = strtoupper($settingName); + if ($settingValue !== '') { + switch ($settingName) { + case 'ARGUMENTSEPARATOR': + self::$localeArgumentSeparator = $settingValue; + + break; + } + } + } + } + } + + self::$functionReplaceFromExcel = self::$functionReplaceToExcel + = self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null; + self::$localeLanguage = $locale; + + return true; + } + + return false; + } + + public static function translateSeparator( + string $fromSeparator, + string $toSeparator, + string $formula, + int &$inBracesLevel, + string $openBrace = self::FORMULA_OPEN_FUNCTION_BRACE, + string $closeBrace = self::FORMULA_CLOSE_FUNCTION_BRACE + ): string { + $strlen = mb_strlen($formula); + for ($i = 0; $i < $strlen; ++$i) { + $chr = mb_substr($formula, $i, 1); + switch ($chr) { + case $openBrace: + ++$inBracesLevel; + + break; + case $closeBrace: + --$inBracesLevel; + + break; + case $fromSeparator: + if ($inBracesLevel > 0) { + $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1); + } + } + } + + return $formula; + } + + /** + * @param string[] $from + * @param string[] $to + */ + protected static function translateFormulaBlock( + array $from, + array $to, + string $formula, + int &$inFunctionBracesLevel, + int &$inMatrixBracesLevel, + string $fromSeparator, + string $toSeparator + ): string { + // Function Names + $formula = (string) preg_replace($from, $to, $formula); + + // Temporarily adjust matrix separators so that they won't be confused with function arguments + $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + $formula = self::translateSeparator(',', '!', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + // Function Argument Separators + $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inFunctionBracesLevel); + // Restore matrix separators + $formula = self::translateSeparator('|', ';', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + $formula = self::translateSeparator('!', ',', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + + return $formula; + } + + /** + * @param string[] $from + * @param string[] $to + */ + protected static function translateFormula(array $from, array $to, string $formula, string $fromSeparator, string $toSeparator): string + { + // Convert any Excel function names and constant names to the required language; + // and adjust function argument separators + if (self::$localeLanguage !== 'en_us') { + $inFunctionBracesLevel = 0; + $inMatrixBracesLevel = 0; + // If there is the possibility of separators within a quoted string, then we treat them as literals + if (str_contains($formula, self::FORMULA_STRING_QUOTE)) { + // So instead we skip replacing in any quoted strings by only replacing in every other array element + // after we've exploded the formula + $temp = explode(self::FORMULA_STRING_QUOTE, $formula); + $notWithinQuotes = false; + foreach ($temp as &$value) { + // Only adjust in alternating array entries + $notWithinQuotes = $notWithinQuotes === false; + if ($notWithinQuotes === true) { + $value = self::translateFormulaBlock($from, $to, $value, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator); + } + } + unset($value); + // Then rebuild the formula string + $formula = implode(self::FORMULA_STRING_QUOTE, $temp); + } else { + // If there's no quoted strings, then we do a simple count/replace + $formula = self::translateFormulaBlock($from, $to, $formula, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator); + } + } + + return $formula; + } + + /** @var null|string[] */ + private static ?array $functionReplaceFromExcel; + + /** @var null|string[] */ + private static ?array $functionReplaceToLocale; + + public function translateFormulaToLocale(string $formula): string + { + $formula = preg_replace(self::CALCULATION_REGEXP_STRIP_XLFN_XLWS, '', $formula) ?? ''; + // Build list of function names and constants for translation + if (self::$functionReplaceFromExcel === null) { + self::$functionReplaceFromExcel = []; + foreach (array_keys(self::$localeFunctions) as $excelFunctionName) { + self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/ui'; + } + foreach (array_keys(self::$localeBoolean) as $excelBoolean) { + self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui'; + } + } + + if (self::$functionReplaceToLocale === null) { + self::$functionReplaceToLocale = []; + foreach (self::$localeFunctions as $localeFunctionName) { + self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2'; + } + foreach (self::$localeBoolean as $localeBoolean) { + self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2'; + } + } + + return self::translateFormula( + self::$functionReplaceFromExcel, + self::$functionReplaceToLocale, + $formula, + ',', + self::$localeArgumentSeparator + ); + } + + /** @var null|string[] */ + protected static ?array $functionReplaceFromLocale; + + /** @var null|string[] */ + protected static ?array $functionReplaceToExcel; + + public function translateFormulaToEnglish(string $formula): string + { + if (self::$functionReplaceFromLocale === null) { + self::$functionReplaceFromLocale = []; + foreach (self::$localeFunctions as $localeFunctionName) { + self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/ui'; + } + foreach (self::$localeBoolean as $excelBoolean) { + self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui'; + } + } + + if (self::$functionReplaceToExcel === null) { + self::$functionReplaceToExcel = []; + foreach (array_keys(self::$localeFunctions) as $excelFunctionName) { + self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2'; + } + foreach (array_keys(self::$localeBoolean) as $excelBoolean) { + self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2'; + } + } + + return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ','); + } + + public static function localeFunc(string $function): string + { + if (self::$localeLanguage !== 'en_us') { + $functionName = trim($function, '('); + if (isset(self::$localeFunctions[$functionName])) { + $brace = ($functionName != $function); + $function = self::$localeFunctions[$functionName]; + if ($brace) { + $function .= '('; + } + } + } + + return $function; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Category.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Category.php new file mode 100644 index 0000000..38c19b3 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Category.php @@ -0,0 +1,22 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|int|float + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return Averages::average( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DCount.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DCount.php new file mode 100644 index 0000000..75b21dd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DCount.php @@ -0,0 +1,45 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnError = true): string|int + { + $field = self::fieldExtract($database, $field); + if ($returnError && $field === null) { + return ExcelError::VALUE(); + } + + return Counts::COUNT( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DCountA.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DCountA.php new file mode 100644 index 0000000..23676bf --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DCountA.php @@ -0,0 +1,44 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|int + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return Counts::COUNTA( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DGet.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DGet.php new file mode 100644 index 0000000..719beaa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DGet.php @@ -0,0 +1,50 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): null|float|int|string + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + $columnData = self::getFilteredColumn($database, $field, $criteria); + if (count($columnData) > 1) { + return ExcelError::NAN(); + } + + /** @var array */ + $row = array_pop($columnData); + + return array_pop($row); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DMax.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DMax.php new file mode 100644 index 0000000..a194288 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DMax.php @@ -0,0 +1,45 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnError = true): null|float|string + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return $returnError ? ExcelError::VALUE() : null; + } + + return Maximum::max( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DMin.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DMin.php new file mode 100644 index 0000000..f94e09f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DMin.php @@ -0,0 +1,45 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnError = true): float|string|null + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return $returnError ? ExcelError::VALUE() : null; + } + + return Minimum::min( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DProduct.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DProduct.php new file mode 100644 index 0000000..e7122d6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DProduct.php @@ -0,0 +1,44 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|float + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return MathTrig\Operations::product( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DStDev.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DStDev.php new file mode 100644 index 0000000..a03ee2a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DStDev.php @@ -0,0 +1,45 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): float|string + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return StandardDeviations::STDEV( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DStDevP.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DStDevP.php new file mode 100644 index 0000000..54f1142 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DStDevP.php @@ -0,0 +1,45 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): float|string + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return StandardDeviations::STDEVP( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DSum.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DSum.php new file mode 100644 index 0000000..7396a0f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DSum.php @@ -0,0 +1,44 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnNull = false): null|float|string + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return $returnNull ? null : ExcelError::VALUE(); + } + + return MathTrig\Sum::sumIgnoringStrings( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DVar.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DVar.php new file mode 100644 index 0000000..e056e9e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DVar.php @@ -0,0 +1,47 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + * + * @return float|string (string if result is an error) + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|float + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return Variances::VAR( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DVarP.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DVarP.php new file mode 100644 index 0000000..3bd571f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DVarP.php @@ -0,0 +1,47 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + * + * @return float|string (string if result is an error) + */ + public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|float + { + $field = self::fieldExtract($database, $field); + if ($field === null) { + return ExcelError::VALUE(); + } + + return Variances::VARP( + self::getFilteredColumn($database, $field, $criteria) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php new file mode 100644 index 0000000..052b9f5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -0,0 +1,228 @@ +|int|string $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + * @param mixed[] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + */ + abstract public static function evaluate(array $database, array|null|int|string $field, array $criteria): null|float|int|string; + + /** + * fieldExtract. + * + * Extracts the column ID to use for the data field. + * + * @param mixed[] $database The range of cells that makes up the list or database. + * A database is a list of related data in which rows of related + * information are records, and columns of data are fields. The + * first row of the list contains labels for each column. + * @param mixed $field Indicates which column is used in the function. Enter the + * column label enclosed between double quotation marks, such as + * "Age" or "Yield," or a number (without quotation marks) that + * represents the position of the column within the list: 1 for + * the first column, 2 for the second column, and so on. + */ + protected static function fieldExtract(array $database, mixed $field): ?int + { + /** @var ?string */ + $single = Functions::flattenSingleValue($field); + $field = strtoupper($single ?? ''); + if ($field === '') { + return null; + } + + /** @var callable */ + $callable = 'strtoupper'; + $fieldNames = array_map($callable, array_shift($database)); //* @phpstan-ignore-line + if (is_numeric($field)) { + $field = (int) $field - 1; + if ($field < 0 || $field >= count($fieldNames)) { + return null; + } + + return $field; + } + $key = array_search($field, array_values($fieldNames), true); + + return ($key !== false) ? (int) $key : null; + } + + /** + * filter. + * + * Parses the selection criteria, extracts the database rows that match those criteria, and + * returns that subset of rows. + * + * @param mixed[] $database The range of cells that makes up the list or database. + * A database is a list of related data in which rows of related + * information are records, and columns of data are fields. The + * first row of the list contains labels for each column. + * @param mixed[][] $criteria The range of cells that contains the conditions you specify. + * You can use any range for the criteria argument, as long as it + * includes at least one column label and at least one cell below + * the column label in which you specify a condition for the + * column. + * + * @return mixed[] + */ + protected static function filter(array $database, array $criteria): array + { + /** @var mixed[] */ + $fieldNames = array_shift($database); + $criteriaNames = array_shift($criteria); + + // Convert the criteria into a set of AND/OR conditions with [:placeholders] + /** @var string[] $criteriaNames */ + $query = self::buildQuery($criteriaNames, $criteria); + + // Loop through each row of the database + /** @var mixed[][] $criteriaNames */ + return self::executeQuery($database, $query, $criteriaNames, $fieldNames); + } + + /** + * @param mixed[] $database The range of cells that makes up the list or database + * @param mixed[][] $criteria + * + * @return mixed[] + */ + protected static function getFilteredColumn(array $database, ?int $field, array $criteria): array + { + // reduce the database to a set of rows that match all the criteria + $database = self::filter($database, $criteria); + $defaultReturnColumnValue = ($field === null) ? 1 : null; + + // extract an array of values for the requested column + $columnData = []; + /** @var mixed[] $row */ + foreach ($database as $rowKey => $row) { + $keys = array_keys($row); + $key = ($field === null) ? null : ($keys[$field] ?? null); + $columnKey = $key ?? 'A'; + $columnData[$rowKey][$columnKey] = ($key === null) ? $defaultReturnColumnValue : ($row[$key] ?? $defaultReturnColumnValue); + } + + return $columnData; + } + + /** + * @param string[] $criteriaNames + * @param mixed[][] $criteria + */ + private static function buildQuery(array $criteriaNames, array $criteria): string + { + $baseQuery = []; + foreach ($criteria as $key => $criterion) { + foreach ($criterion as $field => $value) { + $criterionName = $criteriaNames[$field]; + if ($value !== null) { + $condition = self::buildCondition($value, $criterionName); + $baseQuery[$key][] = $condition; + } + } + } + + $rowQuery = array_map( + fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore-line + $baseQuery + ); + + return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : ($rowQuery[0] ?? ''); + } + + private static function buildCondition(mixed $criterion, string $criterionName): string + { + $ifCondition = Functions::ifCondition($criterion); + + // Check for wildcard characters used in the condition + $result = preg_match('/(?[^"]*)(?".*[*?].*")/ui', $ifCondition, $matches); + if ($result !== 1) { + return "[:{$criterionName}]{$ifCondition}"; + } + + $trueFalse = ($matches['operator'] !== '<>'); + $wildcard = WildcardMatch::wildcard($matches['operand']); + $condition = "WILDCARDMATCH([:{$criterionName}],{$wildcard})"; + if ($trueFalse === false) { + $condition = "NOT({$condition})"; + } + + return $condition; + } + + /** + * @param mixed[] $database + * @param mixed[][] $criteria + * @param array $fields + * + * @return mixed[] + */ + private static function executeQuery(array $database, string $query, array $criteria, array $fields): array + { + foreach ($database as $dataRow => $dataValues) { + // Substitute actual values from the database row for our [:placeholders] + $conditions = $query; + foreach ($criteria as $criterion) { + /** @var string $criterion */ + /** @var mixed[] $dataValues */ + $conditions = self::processCondition($criterion, $fields, $dataValues, $conditions); + } + + // evaluate the criteria against the row data + $result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions); + + // If the row failed to meet the criteria, remove it from the database + if ($result !== true) { + unset($database[$dataRow]); + } + } + + return $database; + } + + /** + * @param array $fields + * @param array $dataValues + */ + private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions): string + { + $key = array_search($criterion, $fields, true); + + $dataValue = 'NULL'; + if (is_bool($dataValues[$key])) { + $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; + } elseif ($dataValues[$key] !== null) { + $dataValue = $dataValues[$key]; + // escape quotes if we have a string containing quotes + if (is_string($dataValue) && str_contains($dataValue, '"')) { + $dataValue = str_replace('"', '""', $dataValue); + } + if (is_string($dataValue)) { + $dataValue = Calculation::wrapResult(strtoupper($dataValue)); + } + $dataValue = StringHelper::convertToString($dataValue); + } + + return str_replace('[:' . $criterion . ']', $dataValue, $conditions); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php new file mode 100644 index 0000000..1165eb1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php @@ -0,0 +1,38 @@ + self::DOW_SUNDAY, + self::DOW_MONDAY, + self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY, + self::DOW_TUESDAY, + self::DOW_WEDNESDAY, + self::DOW_THURSDAY, + self::DOW_FRIDAY, + self::DOW_SATURDAY, + self::DOW_SUNDAY, + self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO, + ]; +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php new file mode 100644 index 0000000..088e379 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php @@ -0,0 +1,60 @@ +format('c')); + + return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray, true) : ExcelError::VALUE(); + } + + /** + * DATETIMENOW. + * + * Returns the current date and time. + * The NOW function is useful when you need to display the current date and time on a worksheet or + * calculate a value based on the current date and time, and have that value updated each time you + * open the worksheet. + * + * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date + * and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. + * + * Excel Function: + * NOW() + * + * @return DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + */ + public static function now(): DateTime|float|int|string + { + $dti = new DateTimeImmutable(); + $dateArray = Helpers::dateParse($dti->format('c')); + + return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : ExcelError::VALUE(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php new file mode 100644 index 0000000..6ee9d7e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php @@ -0,0 +1,179 @@ +|float|int|string $year The value of the year argument can include one to four digits. + * Excel interprets the year argument according to the configured + * date system: 1900 or 1904. + * If year is between 0 (zero) and 1899 (inclusive), Excel adds that + * value to 1900 to calculate the year. For example, DATE(108,1,2) + * returns January 2, 2008 (1900+108). + * If year is between 1900 and 9999 (inclusive), Excel uses that + * value as the year. For example, DATE(2008,1,2) returns January 2, + * 2008. + * If year is less than 0 or is 10000 or greater, Excel returns the + * #NUM! error value. + * @param array|float|int|string $month A positive or negative integer representing the month of the year + * from 1 to 12 (January to December). + * If month is greater than 12, month adds that number of months to + * the first month in the year specified. For example, DATE(2008,14,2) + * returns the serial number representing February 2, 2009. + * If month is less than 1, month subtracts the magnitude of that + * number of months, plus 1, from the first month in the year + * specified. For example, DATE(2008,-3,2) returns the serial number + * representing September 2, 2007. + * @param array|float|int|string $day A positive or negative integer representing the day of the month + * from 1 to 31. + * If day is greater than the number of days in the month specified, + * day adds that number of days to the first day in the month. For + * example, DATE(2008,1,35) returns the serial number representing + * February 4, 2008. + * If day is less than 1, day subtracts the magnitude that number of + * days, plus one, from the first day of the month specified. For + * example, DATE(2008,1,-15) returns the serial number representing + * December 16, 2007. + * + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function fromYMD(array|float|int|string $year, null|array|bool|float|int|string $month, array|float|int|string $day): float|int|DateTime|string|array + { + if (is_array($year) || is_array($month) || is_array($day)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $year, $month, $day); + } + + $baseYear = SharedDateHelper::getExcelCalendar(); + + try { + $year = self::getYear($year, $baseYear); + $month = self::getMonth($month); + $day = self::getDay($day); + self::adjustYearMonth($year, $month, $baseYear); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $excelDateValue = SharedDateHelper::formattedPHPToExcel($year, $month, $day); + + return Helpers::returnIn3FormatsFloat($excelDateValue); + } + + /** + * Convert year from multiple formats to int. + */ + private static function getYear(mixed $year, int $baseYear): int + { + if ($year === null) { + $year = 0; + } elseif (is_scalar($year)) { + $year = StringHelper::testStringAsNumeric((string) $year); + } + if (!is_numeric($year)) { + throw new Exception(ExcelError::VALUE()); + } + $year = (int) $year; + + if ($year < ($baseYear - 1900)) { + throw new Exception(ExcelError::NAN()); + } + if ((($baseYear - 1900) !== 0) && ($year < $baseYear) && ($year >= 1900)) { + throw new Exception(ExcelError::NAN()); + } + + if (($year < $baseYear) && ($year >= ($baseYear - 1900))) { + $year += 1900; + } + + return (int) $year; + } + + /** + * Convert month from multiple formats to int. + */ + private static function getMonth(mixed $month): int + { + if (is_string($month)) { + if (!is_numeric($month)) { + $month = SharedDateHelper::monthStringToNumber($month); + } + } elseif ($month === null) { + $month = 0; + } elseif (is_bool($month)) { + $month = (int) $month; + } + if (!is_numeric($month)) { + throw new Exception(ExcelError::VALUE()); + } + + return (int) $month; + } + + /** + * Convert day from multiple formats to int. + */ + private static function getDay(mixed $day): int + { + if (is_string($day) && !is_numeric($day)) { + $day = SharedDateHelper::dayStringToNumber($day); + } + + if ($day === null) { + $day = 0; + } elseif (is_scalar($day)) { + $day = StringHelper::testStringAsNumeric((string) $day); + } + if (!is_numeric($day)) { + throw new Exception(ExcelError::VALUE()); + } + + return (int) $day; + } + + private static function adjustYearMonth(int &$year, int &$month, int $baseYear): void + { + if ($month < 1) { + // Handle year/month adjustment if month < 1 + --$month; + $year += (int) (ceil($month / 12) - 1); + $month = 13 - abs($month % 12); + } elseif ($month > 12) { + // Handle year/month adjustment if month > 12 + $year += intdiv($month, 12); + $month = ($month % 12); + } + + // Re-validate the year parameter after adjustments + if (($year < $baseYear) || ($year >= 10000)) { + throw new Exception(ExcelError::NAN()); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php new file mode 100644 index 0000000..fc23904 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php @@ -0,0 +1,154 @@ +|int|string Day of the month + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function day(mixed $dateValue): array|int|string + { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + + $weirdResult = self::weirdCondition($dateValue); + if ($weirdResult >= 0) { + return $weirdResult; + } + + try { + $dateValue = Helpers::getDateValue($dateValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + SharedDateHelper::roundMicroseconds($PHPDateObject); + + return (int) $PHPDateObject->format('j'); + } + + /** + * MONTHOFYEAR. + * + * Returns the month of a date represented by a serial number. + * The month is given as an integer, ranging from 1 (January) to 12 (December). + * + * Excel Function: + * MONTH(dateValue) + * + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * + * @return array|int|string Month of the year + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function month(mixed $dateValue): array|string|int + { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + + try { + $dateValue = Helpers::getDateValue($dateValue); + } catch (Exception $e) { + return $e->getMessage(); + } + if ($dateValue < 1 && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900) { + return 1; + } + + // Execute function + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + SharedDateHelper::roundMicroseconds($PHPDateObject); + + return (int) $PHPDateObject->format('n'); + } + + /** + * YEAR. + * + * Returns the year corresponding to a date. + * The year is returned as an integer in the range 1900-9999. + * + * Excel Function: + * YEAR(dateValue) + * + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * + * @return array|int|string Year + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function year(mixed $dateValue): array|string|int + { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + + try { + $dateValue = Helpers::getDateValue($dateValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($dateValue < 1 && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900) { + return 1900; + } + // Execute function + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + SharedDateHelper::roundMicroseconds($PHPDateObject); + + return (int) $PHPDateObject->format('Y'); + } + + /** + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + */ + private static function weirdCondition(mixed $dateValue): int + { + // Excel does not treat 0 consistently for DAY vs. (MONTH or YEAR) + if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900 && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { + if (is_bool($dateValue)) { + return (int) $dateValue; + } + if ($dateValue === null) { + return 0; + } + if (is_numeric($dateValue) && $dateValue < 1 && $dateValue >= 0) { + return 0; + } + } + + return -1; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php new file mode 100644 index 0000000..4035692 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -0,0 +1,176 @@ +|bool|float|int|string $dateValue Text that represents a date in a Microsoft Excel date format. + * For example, "1/30/2008" or "30-Jan-2008" are text strings within + * quotation marks that represent dates. Using the default date + * system in Excel for Windows, date_text must represent a date from + * January 1, 1900, to December 31, 9999. Using the default date + * system in Excel for the Macintosh, date_text must represent a date + * from January 1, 1904, to December 31, 9999. DATEVALUE returns the + * #VALUE! error value if date_text is out of this range. + * Or can be an array of date values + * + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function fromString(null|array|string|int|bool|float $dateValue): array|string|float|int|DateTime + { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + + // try to parse as date iff there is at least one digit + if (is_string($dateValue) && preg_match('/\d/', $dateValue) !== 1) { + return ExcelError::VALUE(); + } + + $dti = new DateTimeImmutable(); + $baseYear = SharedDateHelper::getExcelCalendar(); + $dateValue = trim((string) $dateValue, '"'); + // Strip any ordinals because they're allowed in Excel (English only) + $dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); + // Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany) + $dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue); + + $yearFound = false; + $t1 = explode(' ', $dateValue); + $t = ''; + foreach ($t1 as &$t) { + if ((is_numeric($t)) && ($t > 31)) { + if ($yearFound) { + return ExcelError::VALUE(); + } + if ($t < 100) { + $t += 1900; + } + $yearFound = true; + } + } + if (count($t1) === 1) { + // We've been fed a time value without any date + return ((!str_contains((string) $t, ':'))) ? ExcelError::Value() : 0.0; + } + unset($t); + + $dateValue = self::t1ToString($t1, $dti, $yearFound); + + $PHPDateArray = self::setUpArray($dateValue, $dti); + + return self::finalResults($PHPDateArray, $dti, $baseYear); + } + + /** @param mixed[] $t1 */ + private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string + { + if (count($t1) == 2) { + // We only have two parts of the date: either day/month or month/year + if ($yearFound) { + array_unshift($t1, 1); + } else { + if (is_numeric($t1[1]) && $t1[1] > 29) { + $t1[1] += 1900; + array_unshift($t1, 1); + } else { + $t1[] = $dti->format('Y'); + } + } + } + $dateValue = implode(' ', $t1); + + return $dateValue; + } + + /** + * Parse date. + * + * @return mixed[] + */ + private static function setUpArray(string $dateValue, DateTimeImmutable $dti): array + { + $PHPDateArray = Helpers::dateParse($dateValue); + if (!Helpers::dateParseSucceeded($PHPDateArray)) { + // If original count was 1, we've already returned. + // If it was 2, we added another. + // Therefore, neither of the first 2 strtoks below can fail. + $testVal1 = strtok($dateValue, '- '); + $testVal2 = strtok('- '); + $testVal3 = strtok('- ') ?: $dti->format('Y'); + Helpers::adjustYear((string) $testVal1, (string) $testVal2, $testVal3); + $PHPDateArray = Helpers::dateParse($testVal1 . '-' . $testVal2 . '-' . $testVal3); + if (!Helpers::dateParseSucceeded($PHPDateArray)) { + $PHPDateArray = Helpers::dateParse($testVal2 . '-' . $testVal1 . '-' . $testVal3); + } + } + + return $PHPDateArray; + } + + /** + * Final results. + * + * @param mixed[] $PHPDateArray + * + * @return DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + */ + private static function finalResults(array $PHPDateArray, DateTimeImmutable $dti, int $baseYear): string|float|int|DateTime + { + $retValue = ExcelError::Value(); + if (Helpers::dateParseSucceeded($PHPDateArray)) { + /** @var array{year: int, month: int, day: int, hour: int, minute: int, second: int} $PHPDateArray */ + // Execute function + Helpers::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); + if ($PHPDateArray['year'] < $baseYear) { + return ExcelError::VALUE(); + } + Helpers::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); + Helpers::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); + /** @var array{year: int, month: int, day: int, hour: int, minute: int, second: int} $PHPDateArray */ + $PHPDateArray['hour'] = 0; + $PHPDateArray['minute'] = 0; + $PHPDateArray['second'] = 0; + $month = self::getInt($PHPDateArray, 'month'); + $day = self::getInt($PHPDateArray, 'day'); + $year = self::getInt($PHPDateArray, 'year'); + if (!checkdate($month, $day, $year)) { + return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE(); + } + $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true); + } + + return $retValue; + } + + /** @param mixed[] $array */ + private static function getInt(array $array, string $index): int + { + return (array_key_exists($index, $array) && is_numeric($array[$index])) ? (int) $array[$index] : 0; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php new file mode 100644 index 0000000..a3e5058 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php @@ -0,0 +1,62 @@ +|DateTimeInterface|float|int|string $endDate Excel date serial value (float), + * PHP date timestamp (integer), PHP DateTime object, or a standard date string + * Or can be an array of date values + * @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float), + * PHP date timestamp (integer), PHP DateTime object, or a standard date string + * Or can be an array of date values + * + * @return array|int|string Number of days between start date and end date or an error + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function between(array|DateTimeInterface|float|int|string $endDate, array|DateTimeInterface|float|int|string $startDate): array|int|string + { + if (is_array($endDate) || is_array($startDate)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $endDate, $startDate); + } + + try { + $startDate = Helpers::getDateValue($startDate); + $endDate = Helpers::getDateValue($endDate); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); + $PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); + + $days = ExcelError::VALUE(); + $diff = $PHPStartDateObject->diff($PHPEndDateObject); + if (!is_bool($diff->days)) { + $days = $diff->days; + if ($diff->invert) { + $days = -$days; + } + } + + return $days; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php new file mode 100644 index 0000000..58eb68d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php @@ -0,0 +1,118 @@ +|int|string Number of days between start date and end date + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function between(mixed $startDate = 0, mixed $endDate = 0, mixed $method = false): array|string|int + { + if (is_array($startDate) || is_array($endDate) || is_array($method)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); + } + + try { + $startDate = Helpers::getDateValue($startDate); + $endDate = Helpers::getDateValue($endDate); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (!is_bool($method)) { + return ExcelError::VALUE(); + } + + // Execute function + $PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); + $startDay = $PHPStartDateObject->format('j'); + $startMonth = $PHPStartDateObject->format('n'); + $startYear = $PHPStartDateObject->format('Y'); + + $PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); + $endDay = $PHPEndDateObject->format('j'); + $endMonth = $PHPEndDateObject->format('n'); + $endYear = $PHPEndDateObject->format('Y'); + + return self::dateDiff360((int) $startDay, (int) $startMonth, (int) $startYear, (int) $endDay, (int) $endMonth, (int) $endYear, !$method); + } + + /** + * Return the number of days between two dates based on a 360-day calendar. + */ + private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int + { + $startDay = self::getStartDay($startDay, $startMonth, $startYear, $methodUS); + $endDay = self::getEndDay($endDay, $endMonth, $endYear, $startDay, $methodUS); + + return $endDay + $endMonth * 30 + $endYear * 360 - $startDay - $startMonth * 30 - $startYear * 360; + } + + private static function getStartDay(int $startDay, int $startMonth, int $startYear, bool $methodUS): int + { + if ($startDay == 31) { + --$startDay; + } elseif ($methodUS && ($startMonth == 2 && ($startDay == 29 || ($startDay == 28 && !Helpers::isLeapYear($startYear))))) { + $startDay = 30; + } + + return $startDay; + } + + private static function getEndDay(int $endDay, int &$endMonth, int &$endYear, int $startDay, bool $methodUS): int + { + if ($endDay == 31) { + if ($methodUS && $startDay != 30) { + $endDay = 1; + if ($endMonth == 12) { + ++$endYear; + $endMonth = 1; + } else { + ++$endMonth; + } + } else { + $endDay = 30; + } + } + + return $endDay; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php new file mode 100644 index 0000000..748586d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php @@ -0,0 +1,153 @@ +|string $unit Or can be an array of unit values + * + * @return array|int|string Interval between the dates + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function interval(mixed $startDate, mixed $endDate, array|string $unit = 'D') + { + if (is_array($startDate) || is_array($endDate) || is_array($unit)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $unit); + } + + try { + $startDate = Helpers::getDateValue($startDate); + $endDate = Helpers::getDateValue($endDate); + $difference = self::initialDiff($startDate, $endDate); + $unit = strtoupper($unit); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); + $startDays = (int) $PHPStartDateObject->format('j'); + //$startMonths = (int) $PHPStartDateObject->format('n'); + $startYears = (int) $PHPStartDateObject->format('Y'); + + $PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); + $endDays = (int) $PHPEndDateObject->format('j'); + //$endMonths = (int) $PHPEndDateObject->format('n'); + $endYears = (int) $PHPEndDateObject->format('Y'); + + $PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject); + + $retVal = false; + $retVal = self::replaceRetValue($retVal, $unit, 'D') ?? self::datedifD($difference); + $retVal = self::replaceRetValue($retVal, $unit, 'M') ?? self::datedifM($PHPDiffDateObject); + $retVal = self::replaceRetValue($retVal, $unit, 'MD') ?? self::datedifMD($startDays, $endDays, $PHPEndDateObject, $PHPDiffDateObject); + $retVal = self::replaceRetValue($retVal, $unit, 'Y') ?? self::datedifY($PHPDiffDateObject); + $retVal = self::replaceRetValue($retVal, $unit, 'YD') ?? self::datedifYD($difference, $startYears, $endYears, $PHPStartDateObject, $PHPEndDateObject); + $retVal = self::replaceRetValue($retVal, $unit, 'YM') ?? self::datedifYM($PHPDiffDateObject); + + return is_bool($retVal) ? ExcelError::VALUE() : $retVal; + } + + private static function initialDiff(float $startDate, float $endDate): float + { + // Validate parameters + if ($startDate > $endDate) { + throw new Exception(ExcelError::NAN()); + } + + return $endDate - $startDate; + } + + /** + * Decide whether it's time to set retVal. + */ + private static function replaceRetValue(bool|int $retVal, string $unit, string $compare): null|bool|int + { + if ($retVal !== false || $unit !== $compare) { + return $retVal; + } + + return null; + } + + private static function datedifD(float $difference): int + { + return (int) $difference; + } + + private static function datedifM(DateInterval $PHPDiffDateObject): int + { + return 12 * (int) $PHPDiffDateObject->format('%y') + (int) $PHPDiffDateObject->format('%m'); + } + + private static function datedifMD(int $startDays, int $endDays, DateTime $PHPEndDateObject, DateInterval $PHPDiffDateObject): int + { + if ($endDays < $startDays) { + $retVal = $endDays; + $PHPEndDateObject->modify('-' . $endDays . ' days'); + $adjustDays = (int) $PHPEndDateObject->format('j'); + $retVal += ($adjustDays - $startDays); + } else { + $retVal = (int) $PHPDiffDateObject->format('%d'); + } + + return $retVal; + } + + private static function datedifY(DateInterval $PHPDiffDateObject): int + { + return (int) $PHPDiffDateObject->format('%y'); + } + + private static function datedifYD(float $difference, int $startYears, int $endYears, DateTime $PHPStartDateObject, DateTime $PHPEndDateObject): int + { + $retVal = (int) $difference; + if ($endYears > $startYears) { + $isLeapStartYear = $PHPStartDateObject->format('L'); + $wasLeapEndYear = $PHPEndDateObject->format('L'); + + // Adjust end year to be as close as possible as start year + while ($PHPEndDateObject >= $PHPStartDateObject) { + $PHPEndDateObject->modify('-1 year'); + //$endYears = $PHPEndDateObject->format('Y'); + } + $PHPEndDateObject->modify('+1 year'); + + // Get the result + $retVal = (int) $PHPEndDateObject->diff($PHPStartDateObject)->days; + + // Adjust for leap years cases + $isLeapEndYear = $PHPEndDateObject->format('L'); + $limit = new DateTime($PHPEndDateObject->format('Y-02-29')); + if (!$isLeapStartYear && !$wasLeapEndYear && $isLeapEndYear && $PHPEndDateObject >= $limit) { + --$retVal; + } + } + + return (int) $retVal; + } + + private static function datedifYM(DateInterval $PHPDiffDateObject): int + { + return (int) $PHPDiffDateObject->format('%m'); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php new file mode 100644 index 0000000..492447d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -0,0 +1,314 @@ +format('m'); + $oYear = (int) $PHPDateObject->format('Y'); + + $adjustmentMonthsString = (string) $adjustmentMonths; + if ($adjustmentMonths > 0) { + $adjustmentMonthsString = '+' . $adjustmentMonths; + } + if ($adjustmentMonths != 0) { + $PHPDateObject->modify($adjustmentMonthsString . ' months'); + } + $nMonth = (int) $PHPDateObject->format('m'); + $nYear = (int) $PHPDateObject->format('Y'); + + $monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12); + if ($monthDiff != $adjustmentMonths) { + $adjustDays = (int) $PHPDateObject->format('d'); + $adjustDaysString = '-' . $adjustDays . ' days'; + $PHPDateObject->modify($adjustDaysString); + } + + return $PHPDateObject; + } + + /** + * Help reduce perceived complexity of some tests. + */ + public static function replaceIfEmpty(mixed &$value, mixed $altValue): void + { + $value = $value ?: $altValue; + } + + /** + * Adjust year in ambiguous situations. + */ + public static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void + { + if (!is_numeric($testVal1) || $testVal1 < 31) { + if (!is_numeric($testVal2) || $testVal2 < 12) { + if (is_numeric($testVal3) && $testVal3 < 12) { + $testVal3 = (string) ($testVal3 + 2000); + } + } + } + } + + /** + * Return result in one of three formats. + * + * @param array{year: int, month: int, day: int, hour: int, minute: int, second: int} $dateArray + */ + public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return new DateTime( + $dateArray['year'] + . '-' . $dateArray['month'] + . '-' . $dateArray['day'] + . ' ' . $dateArray['hour'] + . ':' . $dateArray['minute'] + . ':' . $dateArray['second'] + ); + } + $excelDateValue + = SharedDateHelper::formattedPHPToExcel( + $dateArray['year'], + $dateArray['month'], + $dateArray['day'], + $dateArray['hour'], + $dateArray['minute'], + $dateArray['second'] + ); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $noFrac ? floor($excelDateValue) : $excelDateValue; + } + // RETURNDATE_UNIX_TIMESTAMP) + + return SharedDateHelper::excelToTimestamp($excelDateValue); + } + + /** + * Return result in one of three formats. + */ + public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $excelDateValue; + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return SharedDateHelper::excelToTimestamp($excelDateValue); + } + // RETURNDATE_PHP_DATETIME_OBJECT + + return SharedDateHelper::excelToDateTimeObject($excelDateValue); + } + + /** + * Return result in one of three formats. + */ + public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return $PHPDateObject; + } + if ($retType === Functions::RETURNDATE_EXCEL) { + return (float) SharedDateHelper::PHPToExcel($PHPDateObject); + } + // RETURNDATE_UNIX_TIMESTAMP + $stamp = SharedDateHelper::PHPToExcel($PHPDateObject); + $stamp = is_bool($stamp) ? ((int) $stamp) : $stamp; + + return SharedDateHelper::excelToTimestamp($stamp); + } + + private static function baseDate(): int + { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + return 0; + } + if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) { + return 0; + } + + return 1; + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + */ + public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true): void + { + $number = Functions::flattenSingleValue($number); + $nullVal = self::baseDate(); + if ($number === null) { + $number = $nullVal; + } elseif ($allowBool && is_bool($number)) { + $number = $nullVal + (int) $number; + } + } + + /** + * Many functions accept null argument treated as 0. + */ + public static function validateNumericNull(mixed $number): int|float + { + $number = Functions::flattenSingleValue($number); + if ($number === null) { + return 0; + } + if (is_int($number)) { + return $number; + } + if (is_numeric($number)) { + return (float) $number; + } + + throw new Exception(ExcelError::VALUE()); + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @phpstan-assert float $number + */ + public static function validateNotNegative(mixed $number): float + { + if (!is_numeric($number)) { + throw new Exception(ExcelError::VALUE()); + } + if ($number >= 0) { + return (float) $number; + } + + throw new Exception(ExcelError::NAN()); + } + + public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void + { + $isoDate = $PHPDateObject->format('c'); + if ($isoDate < '1900-03-01') { + $PHPDateObject->modify($mod); + } + } + + /** @return array{year: int, month: int, day: int, hour: int, minute: int, second: int} */ + public static function dateParse(string $string): array + { + /** @var array{year: int, month: int, day: int, hour: int, minute: int, second: int} */ + $temp = self::forceArray(date_parse($string)); + + return $temp; + } + + /** @param mixed[] $dateArray */ + public static function dateParseSucceeded(array $dateArray): bool + { + return $dateArray['error_count'] === 0; + } + + /** + * Despite documentation, date_parse probably never returns false. + * Just in case, this routine helps guarantee it. + * + * @param array|false $dateArray + * + * @return mixed[] + */ + private static function forceArray(array|bool $dateArray): array + { + return is_array($dateArray) ? $dateArray : ['error_count' => 1]; + } + + public static function floatOrInt(mixed $value): float|int + { + $result = Functions::scalar($value); + + return is_numeric($result) ? ($result + 0) : 0; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php new file mode 100644 index 0000000..74fd50e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php @@ -0,0 +1,104 @@ +|int $adjustmentMonths The number of months before or after start_date. + * A positive value for months yields a future date; + * a negative value yields a past date. + * Or can be an array of adjustment values + * + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function adjust(mixed $dateValue, array|string|bool|float|int $adjustmentMonths): DateTime|float|int|string|array + { + if (is_array($dateValue) || is_array($adjustmentMonths)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); + } + + try { + $dateValue = Helpers::getDateValue($dateValue, false); + $adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); + } catch (Exception $e) { + return $e->getMessage(); + } + $dateValue = floor($dateValue); + $adjustmentMonths = floor($adjustmentMonths); + + // Execute function + $PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths); + + return Helpers::returnIn3FormatsObject($PHPDateObject); + } + + /** + * EOMONTH. + * + * Returns the date value for the last day of the month that is the indicated number of months + * before or after start_date. + * Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. + * + * Excel Function: + * EOMONTH(dateValue,adjustmentMonths) + * + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * @param array|int $adjustmentMonths The number of months before or after start_date. + * A positive value for months yields a future date; + * a negative value yields a past date. + * Or can be an array of adjustment values + * + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function lastDay(mixed $dateValue, array|float|int|bool|string $adjustmentMonths): array|string|DateTime|float|int + { + if (is_array($dateValue) || is_array($adjustmentMonths)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); + } + + try { + $dateValue = Helpers::getDateValue($dateValue, false); + $adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); + } catch (Exception $e) { + return $e->getMessage(); + } + $dateValue = floor($dateValue); + $adjustmentMonths = floor($adjustmentMonths); + + // Execute function + $PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths + 1); + $adjustDays = (int) $PHPDateObject->format('d'); + $adjustDaysString = '-' . $adjustDays . ' days'; + $PHPDateObject->modify($adjustDaysString); + + return Helpers::returnIn3FormatsObject($PHPDateObject); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php new file mode 100644 index 0000000..2f1d96b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php @@ -0,0 +1,119 @@ +|int|string Interval between the dates + * If an array of values is passed for the $startDate or $endDate arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function count(mixed $startDate, mixed $endDate, mixed ...$dateArgs): array|string|int + { + if (is_array($startDate) || is_array($endDate)) { + return self::evaluateArrayArgumentsSubset( + [self::class, __FUNCTION__], + 2, + $startDate, + $endDate, + ...$dateArgs + ); + } + + try { + // Retrieve the mandatory start and end date that are referenced in the function definition + $sDate = Helpers::getDateValue($startDate); + $eDate = Helpers::getDateValue($endDate); + $startDate = min($sDate, $eDate); + $endDate = max($sDate, $eDate); + // Get the optional days + $dateArgs = Functions::flattenArray($dateArgs); + // Test any extra holiday parameters + $holidayArray = []; + foreach ($dateArgs as $holidayDate) { + $holidayArray[] = Helpers::getDateValue($holidayDate); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $startDow = self::calcStartDow($startDate); + $endDow = self::calcEndDow($endDate); + $wholeWeekDays = (int) floor(($endDate - $startDate) / 7) * 5; + $partWeekDays = self::calcPartWeekDays($startDow, $endDow); + + // Test any extra holiday parameters + $holidayCountedArray = []; + foreach ($holidayArray as $holidayDate) { + if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { + if ((Week::day($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) { + --$partWeekDays; + $holidayCountedArray[] = $holidayDate; + } + } + } + + return self::applySign($wholeWeekDays + $partWeekDays, $sDate, $eDate); + } + + private static function calcStartDow(float $startDate): int + { + $startDow = 6 - (int) Week::day($startDate, 2); + if ($startDow < 0) { + $startDow = 5; + } + + return $startDow; + } + + private static function calcEndDow(float $endDate): int + { + $endDow = (int) Week::day($endDate, 2); + if ($endDow >= 6) { + $endDow = 0; + } + + return $endDow; + } + + private static function calcPartWeekDays(int $startDow, int $endDow): int + { + $partWeekDays = $endDow + $startDow; + if ($partWeekDays > 5) { + $partWeekDays -= 5; + } + + return $partWeekDays; + } + + private static function applySign(int $result, float $sDate, float $eDate): int + { + return ($sDate > $eDate) ? -$result : $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php new file mode 100644 index 0000000..65c4d79 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php @@ -0,0 +1,130 @@ +|bool|float|int|string $hour A number from 0 (zero) to 32767 representing the hour. + * Any value greater than 23 will be divided by 24 and the remainder + * will be treated as the hour value. For example, TIME(27,0,0) = + * TIME(3,0,0) = .125 or 3:00 AM. + * @param null|array|bool|float|int|string $minute A number from 0 to 32767 representing the minute. + * Any value greater than 59 will be converted to hours and minutes. + * For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM. + * @param null|array|bool|float|int|string $second A number from 0 to 32767 representing the second. + * Any value greater than 59 will be converted to hours, minutes, + * and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148 + * or 12:33:20 AM + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + * + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function fromHMS(array|int|float|bool|null|string $hour, array|int|float|bool|null|string $minute, array|int|float|bool|null|string $second): array|string|float|int|DateTime + { + if (is_array($hour) || is_array($minute) || is_array($second)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $hour, $minute, $second); + } + + try { + $hour = self::toIntWithNullBool($hour); + $minute = self::toIntWithNullBool($minute); + $second = self::toIntWithNullBool($second); + } catch (Exception $e) { + return $e->getMessage(); + } + + self::adjustSecond($second, $minute); + self::adjustMinute($minute, $hour); + + if ($hour > 23) { + $hour = $hour % 24; + } elseif ($hour < 0) { + return ExcelError::NAN(); + } + + // Execute function + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $calendar = SharedDateHelper::getExcelCalendar(); + $date = (int) ($calendar !== SharedDateHelper::CALENDAR_WINDOWS_1900); + + return (float) SharedDateHelper::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) SharedDateHelper::excelToTimestamp(SharedDateHelper::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 + } + // RETURNDATE_PHP_DATETIME_OBJECT + // Hour has already been normalized (0-23) above + $phpDateObject = new DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); + + return $phpDateObject; + } + + private static function adjustSecond(int &$second, int &$minute): void + { + if ($second < 0) { + $minute += (int) floor($second / 60); + $second = 60 - abs($second % 60); + if ($second == 60) { + $second = 0; + } + } elseif ($second >= 60) { + $minute += intdiv($second, 60); + $second = $second % 60; + } + } + + private static function adjustMinute(int &$minute, int &$hour): void + { + if ($minute < 0) { + $hour += (int) floor($minute / 60); + $minute = 60 - abs($minute % 60); + if ($minute == 60) { + $minute = 0; + } + } elseif ($minute >= 60) { + $hour += intdiv($minute, 60); + $minute = $minute % 60; + } + } + + /** + * @param mixed $value expect int + */ + private static function toIntWithNullBool(mixed $value): int + { + $value = $value ?? 0; + if (is_bool($value)) { + $value = (int) $value; + } + if (!is_numeric($value)) { + throw new Exception(ExcelError::VALUE()); + } + + return (int) $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php new file mode 100644 index 0000000..86007d4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php @@ -0,0 +1,152 @@ +|int|string Hour + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function hour(mixed $timeValue): array|string|int + { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + + try { + Helpers::nullFalseTrueToNumber($timeValue); + if (is_string($timeValue) && !is_numeric($timeValue)) { + $timeValue = Helpers::getTimeValue($timeValue); + } + Helpers::validateNotNegative($timeValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + try { + SharedDateHelper::excelToDateTimeObject($timeValue); + } catch (Throwable) { + return ExcelError::NAN(); + } + $timeValue = fmod($timeValue, 1); + $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); + SharedDateHelper::roundMicroseconds($timeValue); + + return (int) $timeValue->format('H'); + } + + /** + * MINUTE. + * + * Returns the minutes of a time value. + * The minute is given as an integer, ranging from 0 to 59. + * + * Excel Function: + * MINUTE(timeValue) + * + * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard time string + * Or can be an array of date/time values + * + * @return array|int|string Minute + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function minute(mixed $timeValue): array|string|int + { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + + try { + Helpers::nullFalseTrueToNumber($timeValue); + if (is_string($timeValue) && !is_numeric($timeValue)) { + $timeValue = Helpers::getTimeValue($timeValue); + } + Helpers::validateNotNegative($timeValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + try { + SharedDateHelper::excelToDateTimeObject($timeValue); + } catch (Throwable) { + return ExcelError::NAN(); + } + $timeValue = fmod($timeValue, 1); + $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); + SharedDateHelper::roundMicroseconds($timeValue); + + return (int) $timeValue->format('i'); + } + + /** + * SECOND. + * + * Returns the seconds of a time value. + * The minute is given as an integer, ranging from 0 to 59. + * + * Excel Function: + * SECOND(timeValue) + * + * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard time string + * Or can be an array of date/time values + * + * @return array|int|string Second + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function second(mixed $timeValue): array|string|int + { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + + try { + Helpers::nullFalseTrueToNumber($timeValue); + if (is_string($timeValue) && !is_numeric($timeValue)) { + $timeValue = Helpers::getTimeValue($timeValue); + } + Helpers::validateNotNegative($timeValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + try { + SharedDateHelper::excelToDateTimeObject($timeValue); + } catch (Throwable) { + return ExcelError::NAN(); + } + $timeValue = fmod($timeValue, 1); + $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); + SharedDateHelper::roundMicroseconds($timeValue); + + return (int) $timeValue->format('s'); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php new file mode 100644 index 0000000..fc1f72c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -0,0 +1,97 @@ +|bool|float|int|string $timeValue A text string that represents a time in any one of the Microsoft + * Excel time formats; for example, "6:45 PM" and "18:45" text strings + * within quotation marks that represent time. + * Date information in time_text is ignored. + * Or can be an array of date/time values + * + * @return array|Datetime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function fromString(null|array|string|int|bool|float $timeValue): array|string|Datetime|int|float + { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + + // try to parse as time iff there is at least one digit + if (is_string($timeValue) && !Preg::isMatch('/\d/', $timeValue)) { + return ExcelError::VALUE(); + } + + $timeValue = trim((string) $timeValue, '"'); + if (Preg::isMatch(self::EXTRACT_TIME, $timeValue, $matches)) { + if (empty($matches[6])) { // am/pm + $hour = (int) $matches[0]; + $timeValue = ($hour % 24) . $matches[2]; + } elseif ($matches[6] === $matches[7]) { // Excel wants space before am/pm + return ExcelError::VALUE(); + } else { + $timeValue = $matches[0] . 'm'; + } + } + + $PHPDateArray = Helpers::dateParse($timeValue); + $retValue = ExcelError::VALUE(); + if (Helpers::dateParseSucceeded($PHPDateArray)) { + $hour = $PHPDateArray['hour']; + $minute = $PHPDateArray['minute']; + $second = $PHPDateArray['second']; + // OpenOffice-specific code removed - it works just like Excel + $excelDateValue = SharedDateHelper::formattedPHPToExcel(1900, 1, 1, $hour, $minute, $second) - 1; + + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $retValue = (float) $excelDateValue; + } elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + $retValue = (int) SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600; + } else { + $retValue = new Datetime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); + } + } + + return $retValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php new file mode 100644 index 0000000..080a57b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php @@ -0,0 +1,274 @@ +|int $method Week begins on Sunday or Monday + * 1 or omitted Week begins on Sunday. + * 2 Week begins on Monday. + * 11 Week begins on Monday. + * 12 Week begins on Tuesday. + * 13 Week begins on Wednesday. + * 14 Week begins on Thursday. + * 15 Week begins on Friday. + * 16 Week begins on Saturday. + * 17 Week begins on Sunday. + * 21 ISO (Jan. 4 is week 1, begins on Monday). + * Or can be an array of methods + * + * @return array|int|string Week Number + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function number(mixed $dateValue, array|int|string|null $method = Constants::STARTWEEK_SUNDAY): array|int|string + { + if (is_array($dateValue) || is_array($method)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $method); + } + + $origDateValueNull = empty($dateValue); + + try { + $method = self::validateMethod($method); + if ($dateValue === null) { // boolean not allowed + $dateValue = (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 || $method === Constants::DOW_SUNDAY) ? 0 : 1; + } + $dateValue = self::validateDateValue($dateValue); + if (!$dateValue && self::buggyWeekNum1900($method)) { + // This seems to be an additional Excel bug. + return 0; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + if ($method == Constants::STARTWEEK_MONDAY_ISO) { + Helpers::silly1900($PHPDateObject); + + return (int) $PHPDateObject->format('W'); + } + if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { + return 0; + } + Helpers::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches + $dayOfYear = (int) $PHPDateObject->format('z'); + $PHPDateObject->modify('-' . $dayOfYear . ' days'); + $firstDayOfFirstWeek = (int) $PHPDateObject->format('w'); + $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; + $daysInFirstWeek += 7 * !$daysInFirstWeek; + $endFirstWeek = $daysInFirstWeek - 1; + $weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7); + + return (int) $weekOfYear; + } + + /** + * ISOWEEKNUM. + * + * Returns the ISO 8601 week number of the year for a specified date. + * + * Excel Function: + * ISOWEEKNUM(dateValue) + * + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * + * @return array|int|string Week Number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isoWeekNumber(mixed $dateValue): array|int|string + { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + + if (self::apparentBug($dateValue)) { + return 52; + } + + try { + $dateValue = Helpers::getDateValue($dateValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + Helpers::silly1900($PHPDateObject); + + return (int) $PHPDateObject->format('W'); + } + + /** + * WEEKDAY. + * + * Returns the day of the week for a specified date. The day is given as an integer + * ranging from 0 to 7 (dependent on the requested style). + * + * Excel Function: + * WEEKDAY(dateValue[,style]) + * + * @param null|array|bool|float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * @param mixed $style A number that determines the type of return value + * 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). + * 2 Numbers 1 (Monday) through 7 (Sunday). + * 3 Numbers 0 (Monday) through 6 (Sunday). + * Or can be an array of styles + * + * @return array|int|string Day of the week value + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function day(null|array|float|int|string|bool $dateValue, mixed $style = 1): array|string|int + { + if (is_array($dateValue) || is_array($style)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $style); + } + + try { + $dateValue = Helpers::getDateValue($dateValue); + $style = self::validateStyle($style); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + Helpers::silly1900($PHPDateObject); + $DoW = (int) $PHPDateObject->format('w'); + + switch ($style) { + case 1: + ++$DoW; + + break; + case 2: + $DoW = self::dow0Becomes7($DoW); + + break; + case 3: + $DoW = self::dow0Becomes7($DoW) - 1; + + break; + } + + return $DoW; + } + + /** + * @param mixed $style expect int + */ + private static function validateStyle(mixed $style): int + { + if (!is_numeric($style)) { + throw new Exception(ExcelError::VALUE()); + } + $style = (int) $style; + if (($style < 1) || ($style > 3)) { + throw new Exception(ExcelError::NAN()); + } + + return $style; + } + + private static function dow0Becomes7(int $DoW): int + { + return ($DoW === 0) ? 7 : $DoW; + } + + /** + * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + */ + private static function apparentBug(mixed $dateValue): bool + { + if (SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) { + if (is_bool($dateValue)) { + return true; + } + if (is_numeric($dateValue) && !((int) $dateValue)) { + return true; + } + } + + return false; + } + + /** + * Validate dateValue parameter. + */ + private static function validateDateValue(mixed $dateValue): float + { + if (is_bool($dateValue)) { + throw new Exception(ExcelError::VALUE()); + } + + return Helpers::getDateValue($dateValue); + } + + /** + * Validate method parameter. + */ + private static function validateMethod(mixed $method): int + { + if ($method === null) { + $method = Constants::STARTWEEK_SUNDAY; + } + + if (!is_numeric($method)) { + throw new Exception(ExcelError::VALUE()); + } + + $method = (int) $method; + if (!array_key_exists($method, Constants::METHODARR)) { + throw new Exception(ExcelError::NAN()); + } + $method = Constants::METHODARR[$method]; + + return $method; + } + + private static function buggyWeekNum1900(int $method): bool + { + return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900; + } + + private static function buggyWeekNum1904(int $method, bool $origNull, DateTime $dateObject): bool + { + // This appears to be another Excel bug. + + return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 + && !$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php new file mode 100644 index 0000000..8465864 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php @@ -0,0 +1,206 @@ +|mixed $startDate Excel date serial value (float), PHP date timestamp (integer), + * PHP DateTime object, or a standard date string + * Or can be an array of date values + * @param array|int $endDays The number of nonweekend and nonholiday days before or after + * startDate. A positive value for days yields a future date; a + * negative value yields a past date. + * Or can be an array of int values + * @param null|mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation + * + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function date(mixed $startDate, array|int|string $endDays, mixed ...$dateArgs): array|float|int|DateTime|string + { + if (is_array($startDate) || is_array($endDays)) { + return self::evaluateArrayArgumentsSubset( + [self::class, __FUNCTION__], + 2, + $startDate, + $endDays, + ...$dateArgs + ); + } + + // Retrieve the mandatory start date and days that are referenced in the function definition + try { + $startDate = Helpers::getDateValue($startDate); + $endDays = Helpers::validateNumericNull($endDays); + $holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs)); + } catch (Exception $e) { + return $e->getMessage(); + } + + $startDate = (float) floor($startDate); + $endDays = (int) floor($endDays); + // If endDays is 0, we always return startDate + if ($endDays == 0) { + return $startDate; + } + if ($endDays < 0) { + return self::decrementing($startDate, $endDays, $holidayArray); + } + + return self::incrementing($startDate, $endDays, $holidayArray); + } + + /** + * Use incrementing logic to determine Workday. + * + * @param array $holidayArray + */ + private static function incrementing(float $startDate, int $endDays, array $holidayArray): float|int|DateTime + { + // Adjust the start date if it falls over a weekend + $startDoW = self::getWeekDay($startDate, 3); + if ($startDoW >= 5) { + $startDate += 7 - $startDoW; + --$endDays; + } + + // Add endDays + $endDate = (float) $startDate + ((int) ($endDays / 5) * 7); + $endDays = $endDays % 5; + while ($endDays > 0) { + ++$endDate; + // Adjust the calculated end date if it falls over a weekend + $endDow = self::getWeekDay($endDate, 3); + if ($endDow >= 5) { + $endDate += 7 - $endDow; + } + --$endDays; + } + + // Test any extra holiday parameters + if (!empty($holidayArray)) { + $endDate = self::incrementingArray($startDate, $endDate, $holidayArray); + } + + return Helpers::returnIn3FormatsFloat($endDate); + } + + /** @param array $holidayArray */ + private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float + { + $holidayCountedArray = $holidayDates = []; + foreach ($holidayArray as $holidayDate) { + /** @var float $holidayDate */ + if (self::getWeekDay($holidayDate, 3) < 5) { + $holidayDates[] = $holidayDate; + } + } + sort($holidayDates, SORT_NUMERIC); + foreach ($holidayDates as $holidayDate) { + if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { + if (!in_array($holidayDate, $holidayCountedArray)) { + ++$endDate; + $holidayCountedArray[] = $holidayDate; + } + } + // Adjust the calculated end date if it falls over a weekend + $endDoW = self::getWeekDay($endDate, 3); + if ($endDoW >= 5) { + $endDate += 7 - $endDoW; + } + } + + return $endDate; + } + + /** + * Use decrementing logic to determine Workday. + * + * @param array $holidayArray + */ + private static function decrementing(float $startDate, int $endDays, array $holidayArray): float|int|DateTime + { + // Adjust the start date if it falls over a weekend + $startDoW = self::getWeekDay($startDate, 3); + if ($startDoW >= 5) { + $startDate += -$startDoW + 4; + ++$endDays; + } + + // Add endDays + $endDate = (float) $startDate + ((int) ($endDays / 5) * 7); + $endDays = $endDays % 5; + while ($endDays < 0) { + --$endDate; + // Adjust the calculated end date if it falls over a weekend + $endDow = self::getWeekDay($endDate, 3); + if ($endDow >= 5) { + $endDate += 4 - $endDow; + } + ++$endDays; + } + + // Test any extra holiday parameters + if (!empty($holidayArray)) { + $endDate = self::decrementingArray($startDate, $endDate, $holidayArray); + } + + return Helpers::returnIn3FormatsFloat($endDate); + } + + /** @param array $holidayArray */ + private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float + { + $holidayCountedArray = $holidayDates = []; + foreach ($holidayArray as $holidayDate) { + /** @var float $holidayDate */ + if (self::getWeekDay($holidayDate, 3) < 5) { + $holidayDates[] = $holidayDate; + } + } + rsort($holidayDates, SORT_NUMERIC); + foreach ($holidayDates as $holidayDate) { + if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) { + if (!in_array($holidayDate, $holidayCountedArray)) { + --$endDate; + $holidayCountedArray[] = $holidayDate; + } + } + // Adjust the calculated end date if it falls over a weekend + $endDoW = self::getWeekDay($endDate, 3); + /** int $endDoW */ + if ($endDoW >= 5) { + $endDate += -$endDoW + 4; + } + } + + return $endDate; + } + + private static function getWeekDay(float $date, int $wd): int + { + $result = Functions::scalar(Week::day($date, $wd)); + + return is_int($result) ? $result : -1; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php new file mode 100644 index 0000000..6b3d0f4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php @@ -0,0 +1,124 @@ +|int $method Method used for the calculation + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * Or can be an array of methods + * + * @return array|float|int|string fraction of the year, or a string containing an error + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function fraction(mixed $startDate, mixed $endDate, array|int|string|null $method = 0): array|string|int|float + { + if (is_array($startDate) || is_array($endDate) || is_array($method)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); + } + + try { + $method = (int) Helpers::validateNumericNull($method); + $sDate = Helpers::getDateValue($startDate); + $eDate = Helpers::getDateValue($endDate); + $sDate = self::excelBug($sDate, $startDate, $endDate, $method); + $eDate = self::excelBug($eDate, $endDate, $startDate, $method); + $startDate = min($sDate, $eDate); + $endDate = max($sDate, $eDate); + } catch (Exception $e) { + return $e->getMessage(); + } + + return match ($method) { + 0 => Helpers::floatOrInt(Days360::between($startDate, $endDate)) / 360, + 1 => self::method1($startDate, $endDate), + 2 => Helpers::floatOrInt(Difference::interval($startDate, $endDate)) / 360, + 3 => Helpers::floatOrInt(Difference::interval($startDate, $endDate)) / 365, + 4 => Helpers::floatOrInt(Days360::between($startDate, $endDate, true)) / 360, + default => ExcelError::NAN(), + }; + } + + /** + * Excel 1900 calendar treats date argument of null as 1900-01-00. Really. + */ + private static function excelBug(float $sDate, mixed $startDate, mixed $endDate, int $method): float + { + if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE && SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) { + if ($endDate === null && $startDate !== null) { + if (DateParts::month($sDate) == 12 && DateParts::day($sDate) === 31 && $method === 0) { + $sDate += 2; + } else { + ++$sDate; + } + } + } + + return $sDate; + } + + private static function method1(float $startDate, float $endDate): float + { + $days = Helpers::floatOrInt(Difference::interval($startDate, $endDate)); + $startYear = (int) DateParts::year($startDate); + $endYear = (int) DateParts::year($endDate); + $years = $endYear - $startYear + 1; + $startMonth = (int) DateParts::month($startDate); + $startDay = (int) DateParts::day($startDate); + $endMonth = (int) DateParts::month($endDate); + $endDay = (int) DateParts::day($endDate); + $startMonthDay = 100 * $startMonth + $startDay; + $endMonthDay = 100 * $endMonth + $endDay; + if ($years == 1) { + $tmpCalcAnnualBasis = 365 + (int) Helpers::isLeapYear($endYear); + } elseif ($years == 2 && $startMonthDay >= $endMonthDay) { + if (Helpers::isLeapYear($startYear)) { + $tmpCalcAnnualBasis = 365 + (int) ($startMonthDay <= 229); + } elseif (Helpers::isLeapYear($endYear)) { + $tmpCalcAnnualBasis = 365 + (int) ($endMonthDay >= 229); + } else { + $tmpCalcAnnualBasis = 365; + } + } else { + $tmpCalcAnnualBasis = 0; + for ($year = $startYear; $year <= $endYear; ++$year) { + $tmpCalcAnnualBasis += 365 + (int) Helpers::isLeapYear($year); + } + $tmpCalcAnnualBasis /= $years; + } + + return $days / $tmpCalcAnnualBasis; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php new file mode 100644 index 0000000..9b4e6c0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php @@ -0,0 +1,218 @@ +indexStart = (int) array_shift($keys); + $this->rows = $this->rows($arguments); + $this->columns = $this->columns($arguments); + + $this->argumentCount = count($arguments); + $this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns); + + $this->rows = $this->rows($arguments); + $this->columns = $this->columns($arguments); + + if ($this->arrayArguments() > 2) { + throw new Exception('Formulae with more than two array arguments are not supported'); + } + } + + /** @return mixed[] */ + public function arguments(): array + { + return $this->arguments; + } + + public function hasArrayArgument(): bool + { + return $this->arrayArguments() > 0; + } + + public function getFirstArrayArgumentNumber(): int + { + $rowArrays = $this->filterArray($this->rows); + $columnArrays = $this->filterArray($this->columns); + + for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) { + if (isset($rowArrays[$index]) || isset($columnArrays[$index])) { + return ++$index; + } + } + + return 0; + } + + public function getSingleRowVector(): ?int + { + $rowVectors = $this->getRowVectors(); + + return count($rowVectors) === 1 ? array_pop($rowVectors) : null; + } + + /** @return int[] */ + private function getRowVectors(): array + { + $rowVectors = []; + for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) { + if ($this->rows[$index] === 1 && $this->columns[$index] > 1) { + $rowVectors[] = $index; + } + } + + return $rowVectors; + } + + public function getSingleColumnVector(): ?int + { + $columnVectors = $this->getColumnVectors(); + + return count($columnVectors) === 1 ? array_pop($columnVectors) : null; + } + + /** @return int[] */ + private function getColumnVectors(): array + { + $columnVectors = []; + for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) { + if ($this->rows[$index] > 1 && $this->columns[$index] === 1) { + $columnVectors[] = $index; + } + } + + return $columnVectors; + } + + /** @return int[] */ + public function getMatrixPair(): array + { + for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) { + for ($j = $i + 1; $j < $this->argumentCount; ++$j) { + if (isset($this->rows[$i], $this->rows[$j])) { + return [$i, $j]; + } + } + } + + return []; + } + + public function isVector(int $argument): bool + { + return $this->rows[$argument] === 1 || $this->columns[$argument] === 1; + } + + public function isRowVector(int $argument): bool + { + return $this->rows[$argument] === 1; + } + + public function isColumnVector(int $argument): bool + { + return $this->columns[$argument] === 1; + } + + public function rowCount(int $argument): int + { + return $this->rows[$argument]; + } + + public function columnCount(int $argument): int + { + return $this->columns[$argument]; + } + + /** + * @param mixed[] $arguments + * + * @return int[] + */ + private function rows(array $arguments): array + { + return array_map( + fn ($argument): int => is_countable($argument) ? count($argument) : 1, + $arguments + ); + } + + /** + * @param mixed[] $arguments + * + * @return int[] + */ + private function columns(array $arguments): array + { + return array_map( + fn (mixed $argument): int => is_array($argument) && is_array($argument[array_keys($argument)[0]]) + ? count($argument[array_keys($argument)[0]]) + : 1, + $arguments + ); + } + + public function arrayArguments(): int + { + $count = 0; + foreach (array_keys($this->arguments) as $argument) { + if ($this->rows[$argument] > 1 || $this->columns[$argument] > 1) { + ++$count; + } + } + + return $count; + } + + /** + * @param mixed[] $arguments + * @param int[] $rows + * @param int[] $columns + * + * @return mixed[] + */ + private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array + { + foreach ($arguments as $index => $argument) { + if ($rows[$index] === 1 && $columns[$index] === 1) { + while (is_array($argument)) { + $argument = array_pop($argument); + } + $arguments[$index] = $argument; + } + } + + return $arguments; + } + + /** + * @param mixed[] $array + * + * @return mixed[] + */ + private function filterArray(array $array): array + { + return array_filter( + $array, + fn ($value): bool => $value > 1 + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php new file mode 100644 index 0000000..687ab1b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php @@ -0,0 +1,176 @@ +hasArrayArgument() === false) { + return [$method(...$arguments)]; + } + + if (self::$arrayArgumentHelper->arrayArguments() === 1) { + $nthArgument = self::$arrayArgumentHelper->getFirstArrayArgumentNumber(); + + return self::evaluateNthArgumentAsArray($method, $nthArgument, ...$arguments); + } + + $singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector(); + $singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector(); + + if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) { + // Basic logic for a single row vector and a single column vector + return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments); + } + + $matrixPair = self::$arrayArgumentHelper->getMatrixPair(); + if ($matrixPair !== []) { + if ( + (self::$arrayArgumentHelper->isVector($matrixPair[0]) === true + && self::$arrayArgumentHelper->isVector($matrixPair[1]) === false) + || (self::$arrayArgumentHelper->isVector($matrixPair[0]) === false + && self::$arrayArgumentHelper->isVector($matrixPair[1]) === true) + ) { + // Logic for a matrix and a vector (row or column) + return self::evaluateVectorMatrixPair($method, $matrixPair, ...$arguments); + } + + // Logic for matrix/matrix, column vector/column vector or row vector/row vector + return self::evaluateMatrixPair($method, $matrixPair, ...$arguments); + } + + // Still need to work out the logic for more than two array arguments, + // For the moment, we're throwing an Exception when we initialise the ArrayArgumentHelper + return ['#VALUE!']; + } + + /** + * @param int[] $matrixIndexes + * + * @return mixed[] + */ + private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, mixed ...$arguments): array + { + $matrix2 = array_pop($matrixIndexes) ?? throw new Exception('empty array 2'); + /** @var mixed[][] $matrixValues2 */ + $matrixValues2 = $arguments[$matrix2]; + $matrix1 = array_pop($matrixIndexes) ?? throw new Exception('empty array 1'); + /** @var mixed[][] $matrixValues1 */ + $matrixValues1 = $arguments[$matrix1]; + + /** @var non-empty-array */ + $matrix12 = [$matrix1, $matrix2]; + $rows = min(array_map(self::$arrayArgumentHelper->rowCount(...), $matrix12)); + $columns = min(array_map(self::$arrayArgumentHelper->columnCount(...), $matrix12)); + + if ($rows === 1) { + $rows = max(array_map(self::$arrayArgumentHelper->rowCount(...), $matrix12)); + } + if ($columns === 1) { + $columns = max(array_map(self::$arrayArgumentHelper->columnCount(...), $matrix12)); + } + + $result = []; + for ($rowIndex = 0; $rowIndex < $rows; ++$rowIndex) { + for ($columnIndex = 0; $columnIndex < $columns; ++$columnIndex) { + $rowIndex1 = self::$arrayArgumentHelper->isRowVector($matrix1) ? 0 : $rowIndex; + $columnIndex1 = self::$arrayArgumentHelper->isColumnVector($matrix1) ? 0 : $columnIndex; + $value1 = $matrixValues1[$rowIndex1][$columnIndex1]; + $rowIndex2 = self::$arrayArgumentHelper->isRowVector($matrix2) ? 0 : $rowIndex; + $columnIndex2 = self::$arrayArgumentHelper->isColumnVector($matrix2) ? 0 : $columnIndex; + $value2 = $matrixValues2[$rowIndex2][$columnIndex2]; + $arguments[$matrix1] = $value1; + $arguments[$matrix2] = $value2; + + $result[$rowIndex][$columnIndex] = $method(...$arguments); + } + } + + return $result; + } + + /** + * @param array $matrixIndexes + * + * @return mixed[] + */ + private static function evaluateMatrixPair(callable $method, array $matrixIndexes, mixed ...$arguments): array + { + $matrix2 = array_pop($matrixIndexes); + /** @var mixed[][] $matrixValues2 */ + $matrixValues2 = $arguments[$matrix2]; + $matrix1 = array_pop($matrixIndexes); + /** @var mixed[][] $matrixValues1 */ + $matrixValues1 = $arguments[$matrix1]; + + $result = []; + foreach ($matrixValues1 as $rowIndex => $row) { + foreach ($row as $columnIndex => $value1) { + if (isset($matrixValues2[$rowIndex][$columnIndex]) === false) { + continue; + } + + $value2 = $matrixValues2[$rowIndex][$columnIndex]; + $arguments[$matrix1] = $value1; + $arguments[$matrix2] = $value2; + + $result[$rowIndex][$columnIndex] = $method(...$arguments); + } + } + + return $result; + } + + /** @return mixed[] */ + private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, mixed ...$arguments): array + { + $rowVector = Functions::flattenArray($arguments[$rowIndex]); + $columnVector = Functions::flattenArray($arguments[$columnIndex]); + + $result = []; + foreach ($columnVector as $column) { + $rowResults = []; + foreach ($rowVector as $row) { + $arguments[$rowIndex] = $row; + $arguments[$columnIndex] = $column; + + $rowResults[] = $method(...$arguments); + } + $result[] = $rowResults; + } + + return $result; + } + + /** + * Note, offset is from 1 (for the first argument) rather than from 0. + * + * @return mixed[] + */ + private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, mixed ...$arguments): array + { + $values = array_slice($arguments, $nthArgument - 1, 1); + /** @var mixed[] $values */ + $values = array_pop($values); + + $result = []; + foreach ($values as $value) { + $arguments[$nthArgument - 1] = $value; + $result[] = $method(...$arguments); + } + + return $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php new file mode 100644 index 0000000..97e4c95 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php @@ -0,0 +1,201 @@ +branchPruningEnabled = $branchPruningEnabled; + } + + public function clearBranchStore(): void + { + $this->branchStoreKeyCounter = 0; + } + + public function initialiseForLoop(): void + { + $this->currentCondition = null; + $this->currentOnlyIf = null; + $this->currentOnlyIfNot = null; + $this->previousStoreKey = null; + $this->pendingStoreKey = empty($this->storeKeysStack) ? null : end($this->storeKeysStack); + + if ($this->branchPruningEnabled) { + $this->initialiseCondition(); + $this->initialiseThen(); + $this->initialiseElse(); + } + } + + private function initialiseCondition(): void + { + if (isset($this->pendingStoreKey, $this->conditionMap[$this->pendingStoreKey]) && $this->conditionMap[$this->pendingStoreKey]) { + $this->currentCondition = $this->pendingStoreKey; + $stackDepth = count($this->storeKeysStack); + if ($stackDepth > 1) { + // nested if + $this->previousStoreKey = $this->storeKeysStack[$stackDepth - 2]; + } + } + } + + private function initialiseThen(): void + { + if (isset($this->pendingStoreKey, $this->thenMap[$this->pendingStoreKey]) && $this->thenMap[$this->pendingStoreKey]) { + $this->currentOnlyIf = $this->pendingStoreKey; + } elseif ( + isset($this->previousStoreKey, $this->thenMap[$this->previousStoreKey]) + && $this->thenMap[$this->previousStoreKey] + ) { + $this->currentOnlyIf = $this->previousStoreKey; + } + } + + private function initialiseElse(): void + { + if (isset($this->pendingStoreKey, $this->elseMap[$this->pendingStoreKey]) && $this->elseMap[$this->pendingStoreKey]) { + $this->currentOnlyIfNot = $this->pendingStoreKey; + } elseif ( + isset($this->previousStoreKey, $this->elseMap[$this->previousStoreKey]) + && $this->elseMap[$this->previousStoreKey] + ) { + $this->currentOnlyIfNot = $this->previousStoreKey; + } + } + + public function decrementDepth(): void + { + if (!empty($this->pendingStoreKey)) { + --$this->braceDepthMap[$this->pendingStoreKey]; + } + } + + public function incrementDepth(): void + { + if (!empty($this->pendingStoreKey)) { + ++$this->braceDepthMap[$this->pendingStoreKey]; + } + } + + public function functionCall(string $functionName): void + { + if ($this->branchPruningEnabled && ($functionName === 'IF(')) { + // we handle a new if + $this->pendingStoreKey = $this->getUnusedBranchStoreKey(); + $this->storeKeysStack[] = $this->pendingStoreKey; + $this->conditionMap[$this->pendingStoreKey] = true; + $this->braceDepthMap[$this->pendingStoreKey] = 0; + } elseif (!empty($this->pendingStoreKey) && array_key_exists($this->pendingStoreKey, $this->braceDepthMap)) { + // this is not an if but we go deeper + ++$this->braceDepthMap[$this->pendingStoreKey]; + } + } + + public function argumentSeparator(): void + { + if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === 0) { + // We must go to the IF next argument + if ($this->conditionMap[$this->pendingStoreKey]) { + $this->conditionMap[$this->pendingStoreKey] = false; + $this->thenMap[$this->pendingStoreKey] = true; + } elseif ($this->thenMap[$this->pendingStoreKey]) { + $this->thenMap[$this->pendingStoreKey] = false; + $this->elseMap[$this->pendingStoreKey] = true; + } elseif ($this->elseMap[$this->pendingStoreKey]) { + throw new Exception('Reaching fourth argument of an IF'); + } + } + } + + public function closingBrace(mixed $value): void + { + if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === -1) { + // we are closing an IF( + if ($value !== 'IF(') { + throw new Exception('Parser bug we should be in an "IF("'); + } + + if ($this->conditionMap[$this->pendingStoreKey]) { + throw new Exception('We should not be expecting a condition'); + } + + $this->thenMap[$this->pendingStoreKey] = false; + $this->elseMap[$this->pendingStoreKey] = false; + --$this->braceDepthMap[$this->pendingStoreKey]; + array_pop($this->storeKeysStack); + $this->pendingStoreKey = null; + } + } + + public function currentCondition(): ?string + { + return $this->currentCondition; + } + + public function currentOnlyIf(): ?string + { + return $this->currentOnlyIf; + } + + public function currentOnlyIfNot(): ?string + { + return $this->currentOnlyIfNot; + } + + private function getUnusedBranchStoreKey(): string + { + $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter; + ++$this->branchStoreKeyCounter; + + return $storeKeyValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/CyclicReferenceStack.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/CyclicReferenceStack.php new file mode 100644 index 0000000..c50fad6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/CyclicReferenceStack.php @@ -0,0 +1,67 @@ +stack); + } + + /** + * Push a new entry onto the stack. + * + * @param int|string $value The value to test + */ + public function push($value): void + { + $this->stack[$value] = $value; + } + + /** + * Pop the last entry from the stack. + */ + public function pop(): mixed + { + return array_pop($this->stack); + } + + /** + * Test to see if a specified entry exists on the stack. + * + * @param int|string $value The value to test + */ + public function onStack($value): bool + { + return isset($this->stack[$value]); + } + + /** + * Clear the stack. + */ + public function clear(): void + { + $this->stack = []; + } + + /** + * Return an array of all entries on the stack. + * + * @return mixed[] + */ + public function showStack(): array + { + return $this->stack; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php new file mode 100644 index 0000000..c64c8d2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php @@ -0,0 +1,139 @@ +[-+])? *\% *(?[-+])? *(?[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?[-+])? *(?[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *\% *))$~i'; + + // preg_quoted string for major currency symbols, with a %s for locale currency + private const CURRENCY_CONVERSION_LIST = '\$€£¥%s'; + + /** + * Identify whether a string contains a formatted numeric value, + * and convert it to a numeric if it is. + * + * @param float|string $operand string value to test + */ + public static function convertToNumberIfFormatted(float|string &$operand): bool + { + return self::convertToNumberIfNumeric($operand) + || self::convertToNumberIfFraction($operand) + || self::convertToNumberIfPercent($operand) + || self::convertToNumberIfCurrency($operand); + } + + /** + * Identify whether a string contains a numeric value, + * and convert it to a numeric if it is. + * + * @param float|string $operand string value to test + */ + public static function convertToNumberIfNumeric(float|string &$operand): bool + { + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); + $value = preg_replace(['/(\d)' . $thousandsSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim("$operand")); + $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/'); + $value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? ''); + + if (is_numeric($value)) { + $operand = (float) $value; + + return true; + } + + return false; + } + + /** + * Identify whether a string contains a fractional numeric value, + * and convert it to a numeric if it is. + * + * @param string $operand string value to test + */ + public static function convertToNumberIfFraction(float|string &$operand): bool + { + if (is_string($operand) && preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) { + $sign = ($match[1] === '-') ? '-' : '+'; + $wholePart = ($match[3] === '') ? '' : ($sign . $match[3]); + $fractionFormula = '=' . $wholePart . $sign . $match[4]; + /** @var string */ + $operandx = Calculation::getInstance()->_calculateFormulaValue($fractionFormula); + $operand = $operandx; + + return true; + } + + return false; + } + + /** + * Identify whether a string contains a percentage, and if so, + * convert it to a numeric. + * + * @param float|string $operand string value to test + */ + public static function convertToNumberIfPercent(float|string &$operand): bool + { + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); + $value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', trim("$operand")); + $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/'); + $value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? ''); + + $match = []; + if ($value !== null && preg_match(self::STRING_REGEXP_PERCENT, $value, $match, PREG_UNMATCHED_AS_NULL)) { + //Calculate the percentage + $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? ''; + $operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100; + + return true; + } + + return false; + } + + /** + * Identify whether a string contains a currency value, and if so, + * convert it to a numeric. + * + * @param float|string $operand string value to test + */ + public static function convertToNumberIfCurrency(float|string &$operand): bool + { + $currencyRegexp = self::currencyMatcherRegexp(); + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); + $value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', "$operand"); + + $match = []; + if ($value !== null && preg_match($currencyRegexp, $value, $match, PREG_UNMATCHED_AS_NULL)) { + //Determine the sign + $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? ''; + $decimalSeparator = StringHelper::getDecimalSeparator(); + //Cast to a float + $intermediate = (string) ($match['PostfixedValue'] ?? $match['PrefixedValue']); + $intermediate = str_replace($decimalSeparator, '.', $intermediate); + if (is_numeric($intermediate)) { + $operand = (float) ($sign . str_replace($decimalSeparator, '.', $intermediate)); + + return true; + } + } + + return false; + } + + public static function currencyMatcherRegexp(): string + { + $currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode(), '/')); + $decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/'); + + return '~^(?:(?: *(?[-+])? *(?[' . $currencyCodes . ']) *(?[-+])? *(?[0-9]+[' . $decimalSeparator . ']?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?[-+])? *(?[0-9]+' . $decimalSeparator . '?[0-9]*(?:E[-+]?[0-9]*)?) *(?[' . $currencyCodes . ']) *))$~ui'; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/Logger.php new file mode 100644 index 0000000..e82faf5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/Logger.php @@ -0,0 +1,126 @@ +cellStack = $stack; + } + + /** + * Enable/Disable Calculation engine logging. + */ + public function setWriteDebugLog(bool $writeDebugLog): void + { + $this->writeDebugLog = $writeDebugLog; + } + + /** + * Return whether calculation engine logging is enabled or disabled. + */ + public function getWriteDebugLog(): bool + { + return $this->writeDebugLog; + } + + /** + * Enable/Disable echoing of debug log information. + */ + public function setEchoDebugLog(bool $echoDebugLog): void + { + $this->echoDebugLog = $echoDebugLog; + } + + /** + * Return whether echoing of debug log information is enabled or disabled. + */ + public function getEchoDebugLog(): bool + { + return $this->echoDebugLog; + } + + /** + * Write an entry to the calculation engine debug log. + */ + public function writeDebugLog(string $message, mixed ...$args): void + { + // Only write the debug log if logging is enabled + if ($this->writeDebugLog) { + $message = sprintf($message, ...$args); //* @phpstan-ignore-line + $cellReference = implode(' -> ', $this->cellStack->showStack()); + if ($this->echoDebugLog) { + echo $cellReference, + ($this->cellStack->count() > 0 ? ' => ' : ''), + $message, + PHP_EOL; + } + $this->debugLog[] = $cellReference + . ($this->cellStack->count() > 0 ? ' => ' : '') + . $message; + } + } + + /** + * Write a series of entries to the calculation engine debug log. + * + * @param string[] $args + */ + public function mergeDebugLog(array $args): void + { + if ($this->writeDebugLog) { + foreach ($args as $entry) { + $this->writeDebugLog($entry); + } + } + } + + /** + * Clear the calculation engine debug log. + */ + public function clearLog(): void + { + $this->debugLog = []; + } + + /** + * Return the calculation engine debug log. + * + * @return string[] + */ + public function getLog(): array + { + return $this->debugLog; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/Operands/Operand.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/Operands/Operand.php new file mode 100644 index 0000000..87a733f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engine/Operands/Operand.php @@ -0,0 +1,11 @@ +value = $structuredReference; + } + + /** @param string[] $matches */ + public static function fromParser(string $formula, int $index, array $matches): self + { + $val = $matches[0]; + + $srCount = substr_count($val, self::OPEN_BRACE) + - substr_count($val, self::CLOSE_BRACE); + while ($srCount > 0) { + $srIndex = strlen($val); + $srStringRemainder = substr($formula, $index + $srIndex); + $closingPos = strpos($srStringRemainder, self::CLOSE_BRACE); + if ($closingPos === false) { + throw new Exception("Formula Error: No closing ']' to match opening '['"); + } + $srStringRemainder = substr($srStringRemainder, 0, $closingPos + 1); + --$srCount; + if (str_contains($srStringRemainder, self::OPEN_BRACE)) { + ++$srCount; + } + $val .= $srStringRemainder; + } + + return new self($val); + } + + /** + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + public function parse(Cell $cell): string + { + $this->getTableStructure($cell); + $cellRange = ($this->isRowReference()) ? $this->getRowReference($cell) : $this->getColumnReference(); + $sheetName = ''; + $worksheet = $this->table->getWorksheet(); + if ($worksheet !== null && $worksheet !== $cell->getWorksheet()) { + $sheetName = "'" . $worksheet->getTitle() . "'!"; + } + + return $sheetName . $cellRange; + } + + private function isRowReference(): bool + { + return str_contains($this->value, '[@') + || str_contains($this->value, '[' . self::ITEM_SPECIFIER_THIS_ROW . ']'); + } + + /** + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + private function getTableStructure(Cell $cell): void + { + preg_match(self::TABLE_REFERENCE, $this->value, $matches); + + $this->tableName = $matches[1]; + $this->table = ($this->tableName === '') + ? $this->getTableForCell($cell) + : $this->getTableByName($cell); + $this->reference = $matches[2]; + $tableRange = Coordinate::getRangeBoundaries($this->table->getRange()); + + $this->headersRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null; + $this->firstDataRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1]; + $this->totalsRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null; + $this->lastDataRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1]; + + $cellParam = $cell; + $worksheet = $this->table->getWorksheet(); + if ($worksheet !== null && $worksheet !== $cell->getWorksheet()) { + $cellParam = $worksheet->getCell('A1'); + } + $this->columns = $this->getColumns($cellParam, $tableRange); + } + + /** + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + private function getTableForCell(Cell $cell): Table + { + $tables = $cell->getWorksheet()->getTableCollection(); + foreach ($tables as $table) { + /** @var Table $table */ + $range = $table->getRange(); + if ($cell->isInRange($range) === true) { + $this->tableName = $table->getName(); + + return $table; + } + } + + throw new Exception('Table for Structured Reference cannot be identified'); + } + + /** + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + private function getTableByName(Cell $cell): Table + { + $table = $cell->getWorksheet()->getTableByName($this->tableName); + + if ($table === null) { + $spreadsheet = $cell->getWorksheet()->getParent(); + if ($spreadsheet !== null) { + $table = $spreadsheet->getTableByName($this->tableName); + } + } + + if ($table === null) { + throw new Exception("Table {$this->tableName} for Structured Reference cannot be located"); + } + + return $table; + } + + /** + * @param array{array{string, int}, array{string, int}} $tableRange + * + * @return mixed[] + */ + private function getColumns(Cell $cell, array $tableRange): array + { + $worksheet = $cell->getWorksheet(); + $cellReference = $cell->getCoordinate(); + + $columns = []; + $lastColumn = StringHelper::stringIncrement($tableRange[1][0]); + for ($column = $tableRange[0][0]; $column !== $lastColumn; StringHelper::stringIncrement($column)) { + /** @var string $column */ + $columns[$column] = $worksheet + ->getCell($column . ($this->headersRow ?? ($this->firstDataRow - 1))) + ->getCalculatedValue(); + } + + $worksheet->getCell($cellReference); + + return $columns; + } + + private function getRowReference(Cell $cell): string + { + $reference = str_replace("\u{a0}", ' ', $this->reference); + /** @var string $reference */ + $reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference); + + foreach ($this->columns as $columnId => $columnName) { + $columnName = str_replace("\u{a0}", ' ', $columnName); //* @phpstan-ignore-line + $reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId); + } + + return $this->validateParsedReference(trim($reference, '[]@, ')); + } + + private function adjustRowReference(string $columnName, string $reference, Cell $cell, string $columnId): string + { + if ($columnName !== '') { + $cellReference = $columnId . $cell->getRow(); + $pattern1 = '/\[' . preg_quote($columnName, '/') . '\]/miu'; + $pattern2 = '/@' . preg_quote($columnName, '/') . '/miu'; + if (preg_match($pattern1, $reference) === 1) { + $reference = preg_replace($pattern1, $cellReference, $reference); + } elseif (preg_match($pattern2, $reference) === 1) { + $reference = preg_replace($pattern2, $cellReference, $reference); + } + /** @var string $reference */ + } + + return $reference; + } + + /** + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + private function getColumnReference(): string + { + $reference = str_replace("\u{a0}", ' ', $this->reference); + $startRow = ($this->totalsRow === null) ? $this->lastDataRow : $this->totalsRow; + $endRow = ($this->headersRow === null) ? $this->firstDataRow : $this->headersRow; + + [$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow); + $reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow); + + $reference = trim($reference, '[]@, '); + if (substr_count($reference, ':') > 1) { + $cells = explode(':', $reference); + $firstCell = array_shift($cells); + $lastCell = array_pop($cells); + $reference = "{$firstCell}:{$lastCell}"; + } + + return $this->validateParsedReference($reference); + } + + /** + * @throws Exception + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + private function validateParsedReference(string $reference): string + { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . ':' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) { + throw new Exception( + "Invalid Structured Reference {$this->reference} {$reference}", + Exception::CALCULATION_ENGINE_PUSH_TO_STACK + ); + } + } + + return $reference; + } + + private function fullData(int $startRow, int $endRow): string + { + $columns = array_keys($this->columns); + $firstColumn = array_shift($columns); + $lastColumn = (empty($columns)) ? $firstColumn : array_pop($columns); + + return "{$firstColumn}{$startRow}:{$lastColumn}{$endRow}"; + } + + private function getMinimumRow(string $reference): int + { + return match ($reference) { + self::ITEM_SPECIFIER_ALL, self::ITEM_SPECIFIER_HEADERS => $this->headersRow ?? $this->firstDataRow, + self::ITEM_SPECIFIER_DATA => $this->firstDataRow, + self::ITEM_SPECIFIER_TOTALS => $this->totalsRow ?? $this->lastDataRow, + default => $this->headersRow ?? $this->firstDataRow, + }; + } + + private function getMaximumRow(string $reference): int + { + return match ($reference) { + self::ITEM_SPECIFIER_HEADERS => $this->headersRow ?? $this->firstDataRow, + self::ITEM_SPECIFIER_DATA => $this->lastDataRow, + self::ITEM_SPECIFIER_ALL, self::ITEM_SPECIFIER_TOTALS => $this->totalsRow ?? $this->lastDataRow, + default => $this->totalsRow ?? $this->lastDataRow, + }; + } + + public function value(): string + { + return $this->value; + } + + /** + * @return array + */ + private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array + { + $rowsSelected = false; + foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) { + $pattern = '/\[' . $rowReference . '\]/mui'; + if (preg_match($pattern, $reference) === 1) { + if (($rowReference === self::ITEM_SPECIFIER_HEADERS) && ($this->table->getShowHeaderRow() === false)) { + throw new Exception( + 'Table Headers are Hidden, and should not be Referenced', + Exception::CALCULATION_ENGINE_PUSH_TO_STACK + ); + } + $rowsSelected = true; + $startRow = min($startRow, $this->getMinimumRow($rowReference)); + $endRow = max($endRow, $this->getMaximumRow($rowReference)); + $reference = preg_replace($pattern, '', $reference) ?? ''; + } + } + if ($rowsSelected === false) { + // If there isn't any Special Item Identifier specified, then the selection defaults to data rows only. + $startRow = $this->firstDataRow; + $endRow = $this->lastDataRow; + } + + return [$startRow, $endRow]; + } + + private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string + { + $columnsSelected = false; + foreach ($this->columns as $columnId => $columnName) { + $columnName = str_replace("\u{a0}", ' ', $columnName ?? ''); //* @phpstan-ignore-line + $cellFrom = "{$columnId}{$startRow}"; + $cellTo = "{$columnId}{$endRow}"; + $cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}"; + $pattern = '/\[' . preg_quote($columnName, '/') . '\]/mui'; + if (preg_match($pattern, $reference) === 1) { + $columnsSelected = true; + $reference = preg_replace($pattern, $cellReference, $reference); + } + /** @var string $reference */ + } + if ($columnsSelected === false) { + return $this->fullData($startRow, $endRow); + } + + return $reference; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php new file mode 100644 index 0000000..7f67806 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php @@ -0,0 +1,141 @@ +|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BESSELI(mixed $x, mixed $ord): array|string|float + { + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } + + try { + $x = EngineeringValidations::validateFloat($x); + $ord = EngineeringValidations::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($ord < 0) { + return ExcelError::NAN(); + } + + $fResult = self::calculate($x, $ord); + + return (is_nan($fResult)) ? ExcelError::NAN() : $fResult; + } + + private static function calculate(float $x, int $ord): float + { + return match ($ord) { + 0 => self::besselI0($x), + 1 => self::besselI1($x), + default => self::besselI2($x, $ord), + }; + } + + private static function besselI0(float $x): float + { + $ax = abs($x); + + if ($ax < 3.75) { + $y = $x / 3.75; + $y = $y * $y; + + return 1.0 + $y * (3.5156229 + $y * (3.0899424 + $y * (1.2067492 + + $y * (0.2659732 + $y * (0.360768e-1 + $y * 0.45813e-2))))); + } + + $y = 3.75 / $ax; + + return (exp($ax) / sqrt($ax)) * (0.39894228 + $y * (0.1328592e-1 + $y * (0.225319e-2 + $y * (-0.157565e-2 + + $y * (0.916281e-2 + $y * (-0.2057706e-1 + $y * (0.2635537e-1 + + $y * (-0.1647633e-1 + $y * 0.392377e-2)))))))); + } + + private static function besselI1(float $x): float + { + $ax = abs($x); + + if ($ax < 3.75) { + $y = $x / 3.75; + $y = $y * $y; + $ans = $ax * (0.5 + $y * (0.87890594 + $y * (0.51498869 + $y * (0.15084934 + $y * (0.2658733e-1 + + $y * (0.301532e-2 + $y * 0.32411e-3)))))); + + return ($x < 0.0) ? -$ans : $ans; + } + + $y = 3.75 / $ax; + $ans = 0.2282967e-1 + $y * (-0.2895312e-1 + $y * (0.1787654e-1 - $y * 0.420059e-2)); + $ans = 0.39894228 + $y * (-0.3988024e-1 + $y * (-0.362018e-2 + $y * (0.163801e-2 + + $y * (-0.1031555e-1 + $y * $ans)))); + $ans *= exp($ax) / sqrt($ax); + + return ($x < 0.0) ? -$ans : $ans; + } + + private static function besselI2(float $x, int $ord): float + { + if ($x === 0.0) { + return 0.0; + } + + $tox = 2.0 / abs($x); + $bip = 0; + $ans = 0.0; + $bi = 1.0; + + for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) { + $bim = $bip + $j * $tox * $bi; + $bip = $bi; + $bi = $bim; + + if (abs($bi) > 1.0e+12) { + $ans *= 1.0e-12; + $bi *= 1.0e-12; + $bip *= 1.0e-12; + } + + if ($j === $ord) { + $ans = $bip; + } + } + + $ans *= self::besselI0($x) / $bi; + + return ($x < 0.0 && (($ord % 2) === 1)) ? -$ans : $ans; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php new file mode 100644 index 0000000..58ae258 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php @@ -0,0 +1,176 @@ + 8. This code provides a more accurate calculation + * + * @param mixed $x A float value at which to evaluate the function. + * If x is nonnumeric, BESSELJ returns the #VALUE! error value. + * Or can be an array of values + * @param mixed $ord The integer order of the Bessel function. + * If ord is not an integer, it is truncated. + * If $ord is nonnumeric, BESSELJ returns the #VALUE! error value. + * If $ord < 0, BESSELJ returns the #NUM! error value. + * Or can be an array of values + * + * @return array|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BESSELJ(mixed $x, mixed $ord): array|string|float + { + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } + + try { + $x = EngineeringValidations::validateFloat($x); + $ord = EngineeringValidations::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($ord < 0) { + return ExcelError::NAN(); + } + + $fResult = self::calculate($x, $ord); + + return (is_nan($fResult)) ? ExcelError::NAN() : $fResult; + } + + private static function calculate(float $x, int $ord): float + { + return match ($ord) { + 0 => self::besselJ0($x), + 1 => self::besselJ1($x), + default => self::besselJ2($x, $ord), + }; + } + + private static function besselJ0(float $x): float + { + $ax = abs($x); + + if ($ax < 8.0) { + $y = $x * $x; + $ans1 = 57568490574.0 + $y * (-13362590354.0 + $y * (651619640.7 + $y * (-11214424.18 + $y + * (77392.33017 + $y * (-184.9052456))))); + $ans2 = 57568490411.0 + $y * (1029532985.0 + $y * (9494680.718 + $y * (59272.64853 + $y + * (267.8532712 + $y * 1.0)))); + + return $ans1 / $ans2; + } + + $z = 8.0 / $ax; + $y = $z * $z; + $xx = $ax - 0.785398164; + $ans1 = 1.0 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); + $ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y + * (0.7621095161e-6 - $y * 0.934935152e-7))); + + return sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2); + } + + private static function besselJ1(float $x): float + { + $ax = abs($x); + + if ($ax < 8.0) { + $y = $x * $x; + $ans1 = $x * (72362614232.0 + $y * (-7895059235.0 + $y * (242396853.1 + $y + * (-2972611.439 + $y * (15704.48260 + $y * (-30.16036606)))))); + $ans2 = 144725228442.0 + $y * (2300535178.0 + $y * (18583304.74 + $y * (99447.43394 + $y + * (376.9991397 + $y * 1.0)))); + + return $ans1 / $ans2; + } + + $z = 8.0 / $ax; + $y = $z * $z; + $xx = $ax - 2.356194491; + + $ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6)))); + $ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y + * (-0.88228987e-6 + $y * 0.105787412e-6))); + $ans = sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2); + + return ($x < 0.0) ? -$ans : $ans; + } + + private static function besselJ2(float $x, int $ord): float + { + $ax = abs($x); + if ($ax === 0.0) { + return 0.0; + } + + if ($ax > $ord) { + return self::besselj2a($ax, $ord, $x); + } + + return self::besselj2b($ax, $ord, $x); + } + + private static function besselj2a(float $ax, int $ord, float $x): float + { + $tox = 2.0 / $ax; + $bjm = self::besselJ0($ax); + $bj = self::besselJ1($ax); + for ($j = 1; $j < $ord; ++$j) { + $bjp = $j * $tox * $bj - $bjm; + $bjm = $bj; + $bj = $bjp; + } + $ans = $bj; + + return ($x < 0.0 && ($ord % 2) == 1) ? -$ans : $ans; + } + + private static function besselj2b(float $ax, int $ord, float $x): float + { + $tox = 2.0 / $ax; + $jsum = false; + $bjp = $ans = $sum = 0.0; + $bj = 1.0; + for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) { + $bjm = $j * $tox * $bj - $bjp; + $bjp = $bj; + $bj = $bjm; + if (abs($bj) > 1.0e+10) { + $bj *= 1.0e-10; + $bjp *= 1.0e-10; + $ans *= 1.0e-10; + $sum *= 1.0e-10; + } + if ($jsum === true) { + $sum += $bj; + } + $jsum = $jsum === false; + if ($j === $ord) { + $ans = $bjp; + } + } + $sum = 2.0 * $sum - $bj; + $ans /= $sum; + + return ($x < 0.0 && ($ord % 2) === 1) ? -$ans : $ans; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php new file mode 100644 index 0000000..13e85cb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php @@ -0,0 +1,130 @@ +|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BESSELK(mixed $x, mixed $ord): array|string|float + { + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } + + try { + $x = EngineeringValidations::validateFloat($x); + $ord = EngineeringValidations::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($ord < 0) || ($x <= 0.0)) { + return ExcelError::NAN(); + } + + $fBk = self::calculate($x, $ord); + + return (is_nan($fBk)) ? ExcelError::NAN() : $fBk; + } + + private static function calculate(float $x, int $ord): float + { + return match ($ord) { + 0 => self::besselK0($x), + 1 => self::besselK1($x), + default => self::besselK2($x, $ord), + }; + } + + /** + * Mollify Phpstan. + * + * @codeCoverageIgnore + */ + private static function callBesselI(float $x, int $ord): float + { + $rslt = BesselI::BESSELI($x, $ord); + if (!is_float($rslt)) { + throw new Exception('Unexpected array or string'); + } + + return $rslt; + } + + private static function besselK0(float $x): float + { + if ($x <= 2) { + $fNum2 = $x * 0.5; + $y = ($fNum2 * $fNum2); + + return -log($fNum2) * self::callBesselI($x, 0) + + (-0.57721566 + $y * (0.42278420 + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2 + $y + * (0.10750e-3 + $y * 0.74e-5)))))); + } + + $y = 2 / $x; + + return exp(-$x) / sqrt($x) + * (1.25331414 + $y * (-0.7832358e-1 + $y * (0.2189568e-1 + $y * (-0.1062446e-1 + $y + * (0.587872e-2 + $y * (-0.251540e-2 + $y * 0.53208e-3)))))); + } + + private static function besselK1(float $x): float + { + if ($x <= 2) { + $fNum2 = $x * 0.5; + $y = ($fNum2 * $fNum2); + + return log($fNum2) * self::callBesselI($x, 1) + + (1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y + * (-0.110404e-2 + $y * (-0.4686e-4))))))) / $x; + } + + $y = 2 / $x; + + return exp(-$x) / sqrt($x) + * (1.25331414 + $y * (0.23498619 + $y * (-0.3655620e-1 + $y * (0.1504268e-1 + $y * (-0.780353e-2 + $y + * (0.325614e-2 + $y * (-0.68245e-3))))))); + } + + private static function besselK2(float $x, int $ord): float + { + $fTox = 2 / $x; + $fBkm = self::besselK0($x); + $fBk = self::besselK1($x); + for ($n = 1; $n < $ord; ++$n) { + $fBkp = $fBkm + $n * $fTox * $fBk; + $fBkm = $fBk; + $fBk = $fBkp; + } + + return $fBk; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php new file mode 100644 index 0000000..c65a01b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php @@ -0,0 +1,137 @@ +|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BESSELY(mixed $x, mixed $ord): array|string|float + { + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } + + try { + $x = EngineeringValidations::validateFloat($x); + $ord = EngineeringValidations::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($ord < 0) || ($x <= 0.0)) { + return ExcelError::NAN(); + } + + $fBy = self::calculate($x, $ord); + + return (is_nan($fBy)) ? ExcelError::NAN() : $fBy; + } + + private static function calculate(float $x, int $ord): float + { + return match ($ord) { + 0 => self::besselY0($x), + 1 => self::besselY1($x), + default => self::besselY2($x, $ord), + }; + } + + /** + * Mollify Phpstan. + * + * @codeCoverageIgnore + */ + private static function callBesselJ(float $x, int $ord): float + { + $rslt = BesselJ::BESSELJ($x, $ord); + if (!is_float($rslt)) { + throw new Exception('Unexpected array or string'); + } + + return $rslt; + } + + private static function besselY0(float $x): float + { + if ($x < 8.0) { + $y = ($x * $x); + $ans1 = -2957821389.0 + $y * (7062834065.0 + $y * (-512359803.6 + $y * (10879881.29 + $y + * (-86327.92757 + $y * 228.4622733)))); + $ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y + * (47447.26470 + $y * (226.1030244 + $y)))); + + return $ans1 / $ans2 + 0.636619772 * self::callBesselJ($x, 0) * log($x); + } + + $z = 8.0 / $x; + $y = ($z * $z); + $xx = $x - 0.785398164; + $ans1 = 1 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); + $ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 + $y + * (-0.934945152e-7)))); + + return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2); + } + + private static function besselY1(float $x): float + { + if ($x < 8.0) { + $y = ($x * $x); + $ans1 = $x * (-0.4900604943e13 + $y * (0.1275274390e13 + $y * (-0.5153438139e11 + $y + * (0.7349264551e9 + $y * (-0.4237922726e7 + $y * 0.8511937935e4))))); + $ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y + * (0.1020426050e6 + $y * (0.3549632885e3 + $y))))); + + return ($ans1 / $ans2) + 0.636619772 * (self::callBesselJ($x, 1) * log($x) - 1 / $x); + } + + $z = 8.0 / $x; + $y = $z * $z; + $xx = $x - 2.356194491; + $ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6)))); + $ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y + * (-0.88228987e-6 + $y * 0.105787412e-6))); + + return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2); + } + + private static function besselY2(float $x, int $ord): float + { + $fTox = 2.0 / $x; + $fBym = self::besselY0($x); + $fBy = self::besselY1($x); + for ($n = 1; $n < $ord; ++$n) { + $fByp = $n * $fTox * $fBy - $fBym; + $fBym = $fBy; + $fBy = $fByp; + } + + return $fBy; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php new file mode 100644 index 0000000..65192bf --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php @@ -0,0 +1,247 @@ +|bool|float|int|string $number1 Or can be an array of values + * @param null|array|bool|float|int|string $number2 Or can be an array of values + * + * @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BITAND(null|array|bool|float|int|string $number1, null|array|bool|float|int|string $number2): array|string|int|float + { + if (is_array($number1) || is_array($number2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); + } + + try { + $number1 = self::validateBitwiseArgument($number1); + $number2 = self::validateBitwiseArgument($number2); + } catch (Exception $e) { + return $e->getMessage(); + } + $split1 = self::splitNumber($number1); + $split2 = self::splitNumber($number2); + + return self::SPLIT_DIVISOR * ($split1[0] & $split2[0]) + ($split1[1] & $split2[1]); + } + + /** + * BITOR. + * + * Returns the bitwise OR of two integer values. + * + * Excel Function: + * BITOR(number1, number2) + * + * @param null|array|bool|float|int|string $number1 Or can be an array of values + * @param null|array|bool|float|int|string $number2 Or can be an array of values + * + * @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BITOR(null|array|bool|float|int|string $number1, null|array|bool|float|int|string $number2): array|string|int|float + { + if (is_array($number1) || is_array($number2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); + } + + try { + $number1 = self::validateBitwiseArgument($number1); + $number2 = self::validateBitwiseArgument($number2); + } catch (Exception $e) { + return $e->getMessage(); + } + + $split1 = self::splitNumber($number1); + $split2 = self::splitNumber($number2); + + return self::SPLIT_DIVISOR * ($split1[0] | $split2[0]) + ($split1[1] | $split2[1]); + } + + /** + * BITXOR. + * + * Returns the bitwise XOR of two integer values. + * + * Excel Function: + * BITXOR(number1, number2) + * + * @param null|array|bool|float|int|string $number1 Or can be an array of values + * @param null|array|bool|float|int|string $number2 Or can be an array of values + * + * @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BITXOR(null|array|bool|float|int|string $number1, null|array|bool|float|int|string $number2): array|string|int|float + { + if (is_array($number1) || is_array($number2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); + } + + try { + $number1 = self::validateBitwiseArgument($number1); + $number2 = self::validateBitwiseArgument($number2); + } catch (Exception $e) { + return $e->getMessage(); + } + + $split1 = self::splitNumber($number1); + $split2 = self::splitNumber($number2); + + return self::SPLIT_DIVISOR * ($split1[0] ^ $split2[0]) + ($split1[1] ^ $split2[1]); + } + + /** + * BITLSHIFT. + * + * Returns the number value shifted left by shift_amount bits. + * + * Excel Function: + * BITLSHIFT(number, shift_amount) + * + * @param null|array|bool|float|int|string $number Or can be an array of values + * @param null|array|bool|float|int|string $shiftAmount Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BITLSHIFT(null|array|bool|float|int|string $number, null|array|bool|float|int|string $shiftAmount): array|string|float + { + if (is_array($number) || is_array($shiftAmount)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount); + } + + try { + $number = self::validateBitwiseArgument($number); + $shiftAmount = self::validateShiftAmount($shiftAmount); + } catch (Exception $e) { + return $e->getMessage(); + } + + $result = floor($number * (2 ** $shiftAmount)); + if ($result > 2 ** 48 - 1) { + return ExcelError::NAN(); + } + + return $result; + } + + /** + * BITRSHIFT. + * + * Returns the number value shifted right by shift_amount bits. + * + * Excel Function: + * BITRSHIFT(number, shift_amount) + * + * @param null|array|bool|float|int|string $number Or can be an array of values + * @param null|array|bool|float|int|string $shiftAmount Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function BITRSHIFT(null|array|bool|float|int|string $number, null|array|bool|float|int|string $shiftAmount): array|string|float + { + if (is_array($number) || is_array($shiftAmount)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount); + } + + try { + $number = self::validateBitwiseArgument($number); + $shiftAmount = self::validateShiftAmount($shiftAmount); + } catch (Exception $e) { + return $e->getMessage(); + } + + $result = floor($number / (2 ** $shiftAmount)); + if ($result > 2 ** 48 - 1) { // possible because shiftAmount can be negative + return ExcelError::NAN(); + } + + return $result; + } + + /** + * Validate arguments passed to the bitwise functions. + */ + private static function validateBitwiseArgument(mixed $value): float + { + $value = self::nullFalseTrueToNumber($value); + + if (is_numeric($value)) { + $value = (float) $value; + if ($value == floor($value)) { + if (($value > 2 ** 48 - 1) || ($value < 0)) { + throw new Exception(ExcelError::NAN()); + } + + return floor($value); + } + + throw new Exception(ExcelError::NAN()); + } + + throw new Exception(ExcelError::VALUE()); + } + + /** + * Validate arguments passed to the bitwise functions. + */ + private static function validateShiftAmount(mixed $value): int + { + $value = self::nullFalseTrueToNumber($value); + + if (is_numeric($value)) { + if (abs($value + 0) > 53) { + throw new Exception(ExcelError::NAN()); + } + + return (int) $value; + } + + throw new Exception(ExcelError::VALUE()); + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + */ + private static function nullFalseTrueToNumber(mixed &$number): mixed + { + if ($number === null) { + $number = 0; + } elseif (is_bool($number)) { + $number = (int) $number; + } + + return $number; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Compare.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Compare.php new file mode 100644 index 0000000..ac7ef51 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Compare.php @@ -0,0 +1,82 @@ +|bool|float|int|string $a the first number + * Or can be an array of values + * @param array|bool|float|int|string $b The second number. If omitted, b is assumed to be zero. + * Or can be an array of values + * + * @return array|int|string (string in the event of an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function DELTA(array|float|bool|string|int $a, array|float|bool|string|int $b = 0.0): array|string|int + { + if (is_array($a) || is_array($b)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $a, $b); + } + + try { + $a = EngineeringValidations::validateFloat($a); + $b = EngineeringValidations::validateFloat($b); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (int) (abs($a - $b) < 1.0e-15); + } + + /** + * GESTEP. + * + * Excel Function: + * GESTEP(number[,step]) + * + * Returns 1 if number >= step; returns 0 (zero) otherwise + * Use this function to filter a set of values. For example, by summing several GESTEP + * functions you calculate the count of values that exceed a threshold. + * + * @param array|bool|float|int|string $number the value to test against step + * Or can be an array of values + * @param null|array|bool|float|int|string $step The threshold value. If you omit a value for step, GESTEP uses zero. + * Or can be an array of values + * + * @return array|int|string (string in the event of an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function GESTEP(array|float|bool|string|int $number, $step = 0.0): array|string|int + { + if (is_array($number) || is_array($step)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $step); + } + + try { + $number = EngineeringValidations::validateFloat($number); + $step = EngineeringValidations::validateFloat($step ?? 0.0); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (int) ($number >= $step); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Complex.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Complex.php new file mode 100644 index 0000000..83d110c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Complex.php @@ -0,0 +1,120 @@ +|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function COMPLEX(mixed $realNumber = 0.0, mixed $imaginary = 0.0, mixed $suffix = 'i'): array|string + { + if (is_array($realNumber) || is_array($imaginary) || is_array($suffix)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $realNumber, $imaginary, $suffix); + } + + $realNumber = $realNumber ?? 0.0; + $imaginary = $imaginary ?? 0.0; + $suffix = $suffix ?? 'i'; + + try { + $realNumber = EngineeringValidations::validateFloat($realNumber); + $imaginary = EngineeringValidations::validateFloat($imaginary); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($suffix === 'i') || ($suffix === 'j') || ($suffix === '')) { + $complex = new ComplexObject($realNumber, $imaginary, $suffix); + + return (string) $complex; + } + + return ExcelError::VALUE(); + } + + /** + * IMAGINARY. + * + * Returns the imaginary coefficient of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMAGINARY(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the imaginary + * coefficient + * Or can be an array of values + * + * @return array|float|string (string if an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMAGINARY($complexNumber): array|string|float + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return $complex->getImaginary(); + } + + /** + * IMREAL. + * + * Returns the real coefficient of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMREAL(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the real coefficient + * Or can be an array of values + * + * @return array|float|string (string if an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMREAL($complexNumber): array|string|float + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return $complex->getReal(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php new file mode 100644 index 0000000..5dd51be --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php @@ -0,0 +1,592 @@ +|string $complexNumber the complex number for which you want the absolute value + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMABS(array|string $complexNumber): array|float|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return $complex->abs(); + } + + /** + * IMARGUMENT. + * + * Returns the argument theta of a complex number, i.e. the angle in radians from the real + * axis to the representation of the number in polar coordinates. + * + * Excel Function: + * IMARGUMENT(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the argument theta + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMARGUMENT(array|string $complexNumber): array|float|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return ExcelError::DIV0(); + } + + return $complex->argument(); + } + + /** + * IMCONJUGATE. + * + * Returns the complex conjugate of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCONJUGATE(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the conjugate + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMCONJUGATE(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->conjugate(); + } + + /** + * IMCOS. + * + * Returns the cosine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOS(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the cosine + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMCOS(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->cos(); + } + + /** + * IMCOSH. + * + * Returns the hyperbolic cosine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOSH(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosine + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMCOSH(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->cosh(); + } + + /** + * IMCOT. + * + * Returns the cotangent of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOT(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the cotangent + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMCOT(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->cot(); + } + + /** + * IMCSC. + * + * Returns the cosecant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCSC(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the cosecant + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMCSC(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->csc(); + } + + /** + * IMCSCH. + * + * Returns the hyperbolic cosecant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCSCH(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosecant + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMCSCH(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->csch(); + } + + /** + * IMSIN. + * + * Returns the sine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSIN(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the sine + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMSIN(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->sin(); + } + + /** + * IMSINH. + * + * Returns the hyperbolic sine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSINH(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the hyperbolic sine + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMSINH(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->sinh(); + } + + /** + * IMSEC. + * + * Returns the secant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSEC(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the secant + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMSEC(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->sec(); + } + + /** + * IMSECH. + * + * Returns the hyperbolic secant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSECH(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the hyperbolic secant + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMSECH(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->sech(); + } + + /** + * IMTAN. + * + * Returns the tangent of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMTAN(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the tangent + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMTAN(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->tan(); + } + + /** + * IMSQRT. + * + * Returns the square root of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSQRT(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the square root + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMSQRT(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + $theta = self::IMARGUMENT($complexNumber); + if ($theta === ExcelError::DIV0()) { + return '0'; + } + + return (string) $complex->sqrt(); + } + + /** + * IMLN. + * + * Returns the natural logarithm of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLN(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the natural logarithm + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMLN(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return ExcelError::NAN(); + } + + return (string) $complex->ln(); + } + + /** + * IMLOG10. + * + * Returns the common logarithm (base 10) of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLOG10(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the common logarithm + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMLOG10(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return ExcelError::NAN(); + } + + return (string) $complex->log10(); + } + + /** + * IMLOG2. + * + * Returns the base-2 logarithm of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLOG2(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the base-2 logarithm + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMLOG2(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return ExcelError::NAN(); + } + + return (string) $complex->log2(); + } + + /** + * IMEXP. + * + * Returns the exponential of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMEXP(complexNumber) + * + * @param array|string $complexNumber the complex number for which you want the exponential + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMEXP(array|string $complexNumber): array|string + { + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $complex->exp(); + } + + /** + * IMPOWER. + * + * Returns a complex number in x + yi or x + yj text format raised to a power. + * + * Excel Function: + * IMPOWER(complexNumber,realNumber) + * + * @param array|string $complexNumber the complex number you want to raise to a power + * Or can be an array of values + * @param array|float|int|string $realNumber the power to which you want to raise the complex number + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMPOWER(array|string $complexNumber, array|float|int|string $realNumber): array|string + { + if (is_array($complexNumber) || is_array($realNumber)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber, $realNumber); + } + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException) { + return ExcelError::NAN(); + } + + if (!is_numeric($realNumber)) { + return ExcelError::VALUE(); + } + + return (string) $complex->pow((float) $realNumber); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php new file mode 100644 index 0000000..ed66bea --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php @@ -0,0 +1,128 @@ +|string $complexDividend the complex numerator or dividend + * Or can be an array of values + * @param array|string $complexDivisor the complex denominator or divisor + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMDIV(array|string $complexDividend, array|string $complexDivisor): array|string + { + if (is_array($complexDividend) || is_array($complexDivisor)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexDividend, $complexDivisor); + } + + try { + return (string) (new ComplexObject($complexDividend))->divideby(new ComplexObject($complexDivisor)); + } catch (ComplexException) { + return ExcelError::NAN(); + } + } + + /** + * IMSUB. + * + * Returns the difference of two complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMSUB(complexNumber1,complexNumber2) + * + * @param array|string $complexNumber1 the complex number from which to subtract complexNumber2 + * Or can be an array of values + * @param array|string $complexNumber2 the complex number to subtract from complexNumber1 + * Or can be an array of values + * + * @return array|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function IMSUB(array|string $complexNumber1, array|string $complexNumber2): array|string + { + if (is_array($complexNumber1) || is_array($complexNumber2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber1, $complexNumber2); + } + + try { + return (string) (new ComplexObject($complexNumber1))->subtract(new ComplexObject($complexNumber2)); + } catch (ComplexException) { + return ExcelError::NAN(); + } + } + + /** + * IMSUM. + * + * Returns the sum of two or more complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMSUM(complexNumber[,complexNumber[,...]]) + * + * @param string ...$complexNumbers Series of complex numbers to add + */ + public static function IMSUM(...$complexNumbers): string + { + // Return value + $returnValue = new ComplexObject(0.0); + $aArgs = Functions::flattenArray($complexNumbers); + + try { + // Loop through the arguments + foreach ($aArgs as $complex) { + $returnValue = $returnValue->add(new ComplexObject($complex)); + } + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $returnValue; + } + + /** + * IMPRODUCT. + * + * Returns the product of two or more complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMPRODUCT(complexNumber[,complexNumber[,...]]) + * + * @param string ...$complexNumbers Series of complex numbers to multiply + */ + public static function IMPRODUCT(...$complexNumbers): string + { + // Return value + $returnValue = new ComplexObject(1.0); + $aArgs = Functions::flattenArray($complexNumbers); + + try { + // Loop through the arguments + foreach ($aArgs as $complex) { + $returnValue = $returnValue->multiply(new ComplexObject($complex)); + } + } catch (ComplexException) { + return ExcelError::NAN(); + } + + return (string) $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Constants.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Constants.php new file mode 100644 index 0000000..a926db6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/Constants.php @@ -0,0 +1,11 @@ + 10) { + throw new Exception(ExcelError::NAN()); + } + + return (int) $places; + } + + throw new Exception(ExcelError::VALUE()); + } + + /** + * Formats a number base string value with leading zeroes. + * + * @param string $value The "number" to pad + * @param ?int $places The length that we want to pad this value + * + * @return string The padded "number" + */ + protected static function nbrConversionFormat(string $value, ?int $places): string + { + if ($places !== null) { + if (strlen($value) <= $places) { + return substr(str_pad($value, $places, '0', STR_PAD_LEFT), -10); + } + + return ExcelError::NAN(); + } + + return substr($value, -10); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php new file mode 100644 index 0000000..3b84ce3 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php @@ -0,0 +1,163 @@ +|bool|float|int|string $value The binary number (as a string) that you want to convert. The number + * cannot contain more than 10 characters (10 bits). The most significant + * bit of number is the sign bit. The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is not a valid binary number, or if number contains more than + * 10 characters (10 bits), BIN2DEC returns the #NUM! error value. + * Or can be an array of values + * + * @return array|float|int|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toDecimal($value) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = self::validateValue($value); + $value = self::validateBinary($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (strlen($value) == 10 && $value[0] === '1') { + // Two's Complement + $value = substr($value, -9); + + return -(512 - bindec($value)); + } + + return bindec($value); + } + + /** + * toHex. + * + * Return a binary value as hex. + * + * Excel Function: + * BIN2HEX(x[,places]) + * + * @param array|bool|float|int|string $value The binary number (as a string) that you want to convert. The number + * cannot contain more than 10 characters (10 bits). The most significant + * bit of number is the sign bit. The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is not a valid binary number, or if number contains more than + * 10 characters (10 bits), BIN2HEX returns the #NUM! error value. + * Or can be an array of values + * @param null|array|float|int|string $places The number of characters to use. If places is omitted, BIN2HEX uses the + * minimum number of characters necessary. Places is useful for padding the + * return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, BIN2HEX returns the #VALUE! error value. + * If places is negative, BIN2HEX returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toHex($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateBinary($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (strlen($value) == 10 && $value[0] === '1') { + $high2 = substr($value, 0, 2); + $low8 = substr($value, 2); + $xarr = ['00' => '00000000', '01' => '00000001', '10' => 'FFFFFFFE', '11' => 'FFFFFFFF']; + + return $xarr[$high2] . strtoupper(substr('0' . dechex((int) bindec($low8)), -2)); + } + $hexVal = (string) strtoupper(dechex((int) bindec($value))); + + return self::nbrConversionFormat($hexVal, $places); + } + + /** + * toOctal. + * + * Return a binary value as octal. + * + * Excel Function: + * BIN2OCT(x[,places]) + * + * @param array|bool|float|int|string $value The binary number (as a string) that you want to convert. The number + * cannot contain more than 10 characters (10 bits). The most significant + * bit of number is the sign bit. The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is not a valid binary number, or if number contains more than + * 10 characters (10 bits), BIN2OCT returns the #NUM! error value. + * Or can be an array of values + * @param null|array|float|int|string $places The number of characters to use. If places is omitted, BIN2OCT uses the + * minimum number of characters necessary. Places is useful for padding the + * return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, BIN2OCT returns the #VALUE! error value. + * If places is negative, BIN2OCT returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toOctal($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateBinary($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (strlen($value) == 10 && $value[0] === '1') { // Two's Complement + return str_repeat('7', 6) . strtoupper(decoct((int) bindec("11$value"))); + } + $octVal = (string) decoct((int) bindec($value)); + + return self::nbrConversionFormat($octVal, $places); + } + + protected static function validateBinary(string $value): string + { + if ((strlen($value) > preg_match_all('/[01]/', $value)) || (strlen($value) > 10)) { + throw new Exception(ExcelError::NAN()); + } + + return $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php new file mode 100644 index 0000000..3513840 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php @@ -0,0 +1,213 @@ +|bool|float|int|string $value The decimal integer you want to convert. If number is negative, + * valid place values are ignored and DEC2BIN returns a 10-character + * (10-bit) binary number in which the most significant bit is the sign + * bit. The remaining 9 bits are magnitude bits. Negative numbers are + * represented using two's-complement notation. + * If number < -512 or if number > 511, DEC2BIN returns the #NUM! error + * value. + * If number is nonnumeric, DEC2BIN returns the #VALUE! error value. + * If DEC2BIN requires more than places characters, it returns the #NUM! + * error value. + * Or can be an array of values + * @param null|array|float|int|string $places The number of characters to use. If places is omitted, DEC2BIN uses + * the minimum number of characters necessary. Places is useful for + * padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, DEC2BIN returns the #VALUE! error value. + * If places is zero or negative, DEC2BIN returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toBinary($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateDecimal($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + $value = (int) floor((float) $value); + if ($value > self::LARGEST_BINARY_IN_DECIMAL || $value < self::SMALLEST_BINARY_IN_DECIMAL) { + return ExcelError::NAN(); + } + + $r = decbin($value); + // Two's Complement + $r = substr($r, -10); + + return self::nbrConversionFormat($r, $places); + } + + /** + * toHex. + * + * Return a decimal value as hex. + * + * Excel Function: + * DEC2HEX(x[,places]) + * + * @param array|bool|float|int|string $value The decimal integer you want to convert. If number is negative, + * places is ignored and DEC2HEX returns a 10-character (40-bit) + * hexadecimal number in which the most significant bit is the sign + * bit. The remaining 39 bits are magnitude bits. Negative numbers + * are represented using two's-complement notation. + * If number < -549,755,813,888 or if number > 549,755,813,887, + * DEC2HEX returns the #NUM! error value. + * If number is nonnumeric, DEC2HEX returns the #VALUE! error value. + * If DEC2HEX requires more than places characters, it returns the + * #NUM! error value. + * Or can be an array of values + * @param null|array|float|int|string $places The number of characters to use. If places is omitted, DEC2HEX uses + * the minimum number of characters necessary. Places is useful for + * padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, DEC2HEX returns the #VALUE! error value. + * If places is zero or negative, DEC2HEX returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toHex($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateDecimal($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + $value = floor((float) $value); + if ($value > self::LARGEST_HEX_IN_DECIMAL || $value < self::SMALLEST_HEX_IN_DECIMAL) { + return ExcelError::NAN(); + } + $r = strtoupper(dechex((int) $value)); + $r = self::hex32bit($value, $r); + + return self::nbrConversionFormat($r, $places); + } + + public static function hex32bit(float $value, string $hexstr, bool $force = false): string + { + if (PHP_INT_SIZE === 4 || $force) { + if ($value >= 2 ** 32) { + $quotient = (int) ($value / (2 ** 32)); + + return strtoupper(substr('0' . dechex($quotient), -2) . $hexstr); + } + if ($value < -(2 ** 32)) { + $quotient = 256 - (int) ceil((-$value) / (2 ** 32)); + + return strtoupper(substr('0' . dechex($quotient), -2) . substr("00000000$hexstr", -8)); + } + if ($value < 0) { + return "FF$hexstr"; + } + } + + return $hexstr; + } + + /** + * toOctal. + * + * Return a decimal value as octal. + * + * Excel Function: + * DEC2OCT(x[,places]) + * + * @param array|bool|float|int|string $value The decimal integer you want to convert. If number is negative, + * places is ignored and DEC2OCT returns a 10-character (30-bit) + * octal number in which the most significant bit is the sign bit. + * The remaining 29 bits are magnitude bits. Negative numbers are + * represented using two's-complement notation. + * If number < -536,870,912 or if number > 536,870,911, DEC2OCT + * returns the #NUM! error value. + * If number is nonnumeric, DEC2OCT returns the #VALUE! error value. + * If DEC2OCT requires more than places characters, it returns the + * #NUM! error value. + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, DEC2OCT uses + * the minimum number of characters necessary. Places is useful for + * padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, DEC2OCT returns the #VALUE! error value. + * If places is zero or negative, DEC2OCT returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toOctal($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateDecimal($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + $value = (int) floor((float) $value); + if ($value > self::LARGEST_OCTAL_IN_DECIMAL || $value < self::SMALLEST_OCTAL_IN_DECIMAL) { + return ExcelError::NAN(); + } + $r = decoct($value); + $r = substr($r, -10); + + return self::nbrConversionFormat($r, $places); + } + + protected static function validateDecimal(string $value): string + { + if (strlen($value) > preg_match_all('/[-0123456789.]/', $value)) { + throw new Exception(ExcelError::VALUE()); + } + + return $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php new file mode 100644 index 0000000..be8ed39 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php @@ -0,0 +1,175 @@ +|bool|float|string $value The hexadecimal number you want to convert. + * Number cannot contain more than 10 characters. + * The most significant bit of number is the sign bit (40th bit from the right). + * The remaining 9 bits are magnitude bits. + * Negative numbers are represented using two's-complement notation. + * If number is negative, HEX2BIN ignores places and returns a 10-character binary number. + * If number is negative, it cannot be less than FFFFFFFE00, + * and if number is positive, it cannot be greater than 1FF. + * If number is not a valid hexadecimal number, HEX2BIN returns the #NUM! error value. + * If HEX2BIN requires more than places characters, it returns the #NUM! error value. + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, + * HEX2BIN uses the minimum number of characters necessary. Places + * is useful for padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, HEX2BIN returns the #VALUE! error value. + * If places is negative, HEX2BIN returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toBinary($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateHex($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + $dec = self::toDecimal($value); + + return ConvertDecimal::toBinary($dec, $places); + } + + /** + * toDecimal. + * + * Return a hex value as decimal. + * + * Excel Function: + * HEX2DEC(x) + * + * @param array|bool|float|int|string $value The hexadecimal number you want to convert. This number cannot + * contain more than 10 characters (40 bits). The most significant + * bit of number is the sign bit. The remaining 39 bits are magnitude + * bits. Negative numbers are represented using two's-complement + * notation. + * If number is not a valid hexadecimal number, HEX2DEC returns the + * #NUM! error value. + * Or can be an array of values + * + * @return array|float|int|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toDecimal($value) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = self::validateValue($value); + $value = self::validateHex($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (strlen($value) > 10) { + return ExcelError::NAN(); + } + + $binX = ''; + foreach (mb_str_split($value, 1, 'UTF-8') as $char) { + $binX .= str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT); + } + if (strlen($binX) == 40 && $binX[0] == '1') { + for ($i = 0; $i < 40; ++$i) { + $binX[$i] = ($binX[$i] == '1' ? '0' : '1'); + } + + return (bindec($binX) + 1) * -1; + } + + return bindec($binX); + } + + /** + * toOctal. + * + * Return a hex value as octal. + * + * Excel Function: + * HEX2OCT(x[,places]) + * + * @param array|bool|float|int|string $value The hexadecimal number you want to convert. Number cannot + * contain more than 10 characters. The most significant bit of + * number is the sign bit. The remaining 39 bits are magnitude + * bits. Negative numbers are represented using two's-complement + * notation. + * If number is negative, HEX2OCT ignores places and returns a + * 10-character octal number. + * If number is negative, it cannot be less than FFE0000000, and + * if number is positive, it cannot be greater than 1FFFFFFF. + * If number is not a valid hexadecimal number, HEX2OCT returns + * the #NUM! error value. + * If HEX2OCT requires more than places characters, it returns + * the #NUM! error value. + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, HEX2OCT + * uses the minimum number of characters necessary. Places is + * useful for padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, HEX2OCT returns the #VALUE! error + * value. + * If places is negative, HEX2OCT returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toOctal($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateHex($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + $decimal = self::toDecimal($value); + + return ConvertDecimal::toOctal($decimal, $places); + } + + protected static function validateHex(string $value): string + { + if (strlen($value) > preg_match_all('/[0123456789ABCDEF]/', $value)) { + throw new Exception(ExcelError::NAN()); + } + + return $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php new file mode 100644 index 0000000..03a906d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php @@ -0,0 +1,174 @@ +|bool|float|int|string $value The octal number you want to convert. Number may not + * contain more than 10 characters. The most significant + * bit of number is the sign bit. The remaining 29 bits + * are magnitude bits. Negative numbers are represented + * using two's-complement notation. + * If number is negative, OCT2BIN ignores places and returns + * a 10-character binary number. + * If number is negative, it cannot be less than 7777777000, + * and if number is positive, it cannot be greater than 777. + * If number is not a valid octal number, OCT2BIN returns + * the #NUM! error value. + * If OCT2BIN requires more than places characters, it + * returns the #NUM! error value. + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, + * OCT2BIN uses the minimum number of characters necessary. + * Places is useful for padding the return value with + * leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, OCT2BIN returns the #VALUE! + * error value. + * If places is negative, OCT2BIN returns the #NUM! error + * value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toBinary($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateOctal($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + return ConvertDecimal::toBinary(self::toDecimal($value), $places); + } + + /** + * toDecimal. + * + * Return an octal value as decimal. + * + * Excel Function: + * OCT2DEC(x) + * + * @param array|bool|float|int|string $value The octal number you want to convert. Number may not contain + * more than 10 octal characters (30 bits). The most significant + * bit of number is the sign bit. The remaining 29 bits are + * magnitude bits. Negative numbers are represented using + * two's-complement notation. + * If number is not a valid octal number, OCT2DEC returns the + * #NUM! error value. + * Or can be an array of values + * + * @return array|float|int|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toDecimal($value) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = self::validateValue($value); + $value = self::validateOctal($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + $binX = ''; + foreach (mb_str_split($value, 1, 'UTF-8') as $char) { + $binX .= str_pad(decbin((int) $char), 3, '0', STR_PAD_LEFT); + } + if (strlen($binX) == 30 && $binX[0] == '1') { + for ($i = 0; $i < 30; ++$i) { + $binX[$i] = ($binX[$i] == '1' ? '0' : '1'); + } + + return (bindec($binX) + 1) * -1; + } + + return bindec($binX); + } + + /** + * toHex. + * + * Return an octal value as hex. + * + * Excel Function: + * OCT2HEX(x[,places]) + * + * @param array|bool|float|int|string $value The octal number you want to convert. Number may not contain + * more than 10 octal characters (30 bits). The most significant + * bit of number is the sign bit. The remaining 29 bits are + * magnitude bits. Negative numbers are represented using + * two's-complement notation. + * If number is negative, OCT2HEX ignores places and returns a + * 10-character hexadecimal number. + * If number is not a valid octal number, OCT2HEX returns the + * #NUM! error value. + * If OCT2HEX requires more than places characters, it returns + * the #NUM! error value. + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, OCT2HEX + * uses the minimum number of characters necessary. Places is useful + * for padding the return value with leading 0s (zeros). + * If places is not an integer, it is truncated. + * If places is nonnumeric, OCT2HEX returns the #VALUE! error value. + * If places is negative, OCT2HEX returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toHex($value, $places = null): array|string + { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + + try { + $value = self::validateValue($value); + $value = self::validateOctal($value); + $places = self::validatePlaces($places); + } catch (Exception $e) { + return $e->getMessage(); + } + + $hexVal = strtoupper(dechex((int) self::toDecimal($value))); + $hexVal = (PHP_INT_SIZE === 4 && strlen($value) === 10 && $value[0] >= '4') ? "FF{$hexVal}" : $hexVal; + + return self::nbrConversionFormat($hexVal, $places); + } + + protected static function validateOctal(string $value): string + { + $numDigits = (int) preg_match_all('/[01234567]/', $value); + if (strlen($value) > $numDigits || $numDigits > 10) { + throw new Exception(ExcelError::NAN()); + } + + return $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php new file mode 100644 index 0000000..db1161c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php @@ -0,0 +1,686 @@ + + */ + private static array $conversionUnits = [ + // Weight and Mass + 'g' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Gram', 'AllowPrefix' => true], + 'sg' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Slug', 'AllowPrefix' => false], + 'lbm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Pound mass (avoirdupois)', 'AllowPrefix' => false], + 'u' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'U (atomic mass unit)', 'AllowPrefix' => true], + 'ozm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Ounce mass (avoirdupois)', 'AllowPrefix' => false], + 'grain' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Grain', 'AllowPrefix' => false], + 'cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'U.S. (short) hundredweight', 'AllowPrefix' => false], + 'shweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'U.S. (short) hundredweight', 'AllowPrefix' => false], + 'uk_cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial hundredweight', 'AllowPrefix' => false], + 'lcwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial hundredweight', 'AllowPrefix' => false], + 'hweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial hundredweight', 'AllowPrefix' => false], + 'stone' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Stone', 'AllowPrefix' => false], + 'ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Ton', 'AllowPrefix' => false], + 'uk_ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial ton', 'AllowPrefix' => false], + 'LTON' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial ton', 'AllowPrefix' => false], + 'brton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial ton', 'AllowPrefix' => false], + // Distance + 'm' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Meter', 'AllowPrefix' => true], + 'mi' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Statute mile', 'AllowPrefix' => false], + 'Nmi' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Nautical mile', 'AllowPrefix' => false], + 'in' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Inch', 'AllowPrefix' => false], + 'ft' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Foot', 'AllowPrefix' => false], + 'yd' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Yard', 'AllowPrefix' => false], + 'ang' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Angstrom', 'AllowPrefix' => true], + 'ell' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Ell', 'AllowPrefix' => false], + 'ly' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Light Year', 'AllowPrefix' => false], + 'parsec' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Parsec', 'AllowPrefix' => false], + 'pc' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Parsec', 'AllowPrefix' => false], + 'Pica' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Pica (1/72 in)', 'AllowPrefix' => false], + 'Picapt' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Pica (1/72 in)', 'AllowPrefix' => false], + 'pica' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Pica (1/6 in)', 'AllowPrefix' => false], + 'survey_mi' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'U.S survey mile (statute mile)', 'AllowPrefix' => false], + // Time + 'yr' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Year', 'AllowPrefix' => false], + 'day' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Day', 'AllowPrefix' => false], + 'd' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Day', 'AllowPrefix' => false], + 'hr' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Hour', 'AllowPrefix' => false], + 'mn' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Minute', 'AllowPrefix' => false], + 'min' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Minute', 'AllowPrefix' => false], + 'sec' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Second', 'AllowPrefix' => true], + 's' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Second', 'AllowPrefix' => true], + // Pressure + 'Pa' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Pascal', 'AllowPrefix' => true], + 'p' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Pascal', 'AllowPrefix' => true], + 'atm' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Atmosphere', 'AllowPrefix' => true], + 'at' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Atmosphere', 'AllowPrefix' => true], + 'mmHg' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'mm of Mercury', 'AllowPrefix' => true], + 'psi' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'PSI', 'AllowPrefix' => true], + 'Torr' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Torr', 'AllowPrefix' => true], + // Force + 'N' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Newton', 'AllowPrefix' => true], + 'dyn' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Dyne', 'AllowPrefix' => true], + 'dy' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Dyne', 'AllowPrefix' => true], + 'lbf' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Pound force', 'AllowPrefix' => false], + 'pond' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Pond', 'AllowPrefix' => true], + // Energy + 'J' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Joule', 'AllowPrefix' => true], + 'e' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Erg', 'AllowPrefix' => true], + 'c' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Thermodynamic calorie', 'AllowPrefix' => true], + 'cal' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'IT calorie', 'AllowPrefix' => true], + 'eV' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Electron volt', 'AllowPrefix' => true], + 'ev' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Electron volt', 'AllowPrefix' => true], + 'HPh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Horsepower-hour', 'AllowPrefix' => false], + 'hh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Horsepower-hour', 'AllowPrefix' => false], + 'Wh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Watt-hour', 'AllowPrefix' => true], + 'wh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Watt-hour', 'AllowPrefix' => true], + 'flb' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Foot-pound', 'AllowPrefix' => false], + 'BTU' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'BTU', 'AllowPrefix' => false], + 'btu' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'BTU', 'AllowPrefix' => false], + // Power + 'HP' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Horsepower', 'AllowPrefix' => false], + 'h' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Horsepower', 'AllowPrefix' => false], + 'W' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Watt', 'AllowPrefix' => true], + 'w' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Watt', 'AllowPrefix' => true], + 'PS' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Pferdestärke', 'AllowPrefix' => false], + // Magnetism + 'T' => ['Group' => self::CATEGORY_MAGNETISM, 'UnitName' => 'Tesla', 'AllowPrefix' => true], + 'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'UnitName' => 'Gauss', 'AllowPrefix' => true], + // Temperature + 'C' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Celsius', 'AllowPrefix' => false], + 'cel' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Celsius', 'AllowPrefix' => false], + 'F' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Fahrenheit', 'AllowPrefix' => false], + 'fah' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Fahrenheit', 'AllowPrefix' => false], + 'K' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Kelvin', 'AllowPrefix' => false], + 'kel' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Kelvin', 'AllowPrefix' => false], + 'Rank' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Rankine', 'AllowPrefix' => false], + 'Reau' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Réaumur', 'AllowPrefix' => false], + // Volume + 'l' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Litre', 'AllowPrefix' => true], + 'L' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Litre', 'AllowPrefix' => true], + 'lt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Litre', 'AllowPrefix' => true], + 'tsp' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Teaspoon', 'AllowPrefix' => false], + 'tspm' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Modern Teaspoon', 'AllowPrefix' => false], + 'tbs' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Tablespoon', 'AllowPrefix' => false], + 'oz' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Fluid Ounce', 'AllowPrefix' => false], + 'cup' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cup', 'AllowPrefix' => false], + 'pt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'U.S. Pint', 'AllowPrefix' => false], + 'us_pt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'U.S. Pint', 'AllowPrefix' => false], + 'uk_pt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'U.K. Pint', 'AllowPrefix' => false], + 'qt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Quart', 'AllowPrefix' => false], + 'uk_qt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Imperial Quart (UK)', 'AllowPrefix' => false], + 'gal' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Gallon', 'AllowPrefix' => false], + 'uk_gal' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Imperial Gallon (UK)', 'AllowPrefix' => false], + 'ang3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Angstrom', 'AllowPrefix' => true], + 'ang^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Angstrom', 'AllowPrefix' => true], + 'barrel' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'US Oil Barrel', 'AllowPrefix' => false], + 'bushel' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'US Bushel', 'AllowPrefix' => false], + 'in3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Inch', 'AllowPrefix' => false], + 'in^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Inch', 'AllowPrefix' => false], + 'ft3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Foot', 'AllowPrefix' => false], + 'ft^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Foot', 'AllowPrefix' => false], + 'ly3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Light Year', 'AllowPrefix' => false], + 'ly^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Light Year', 'AllowPrefix' => false], + 'm3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Meter', 'AllowPrefix' => true], + 'm^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Meter', 'AllowPrefix' => true], + 'mi3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Mile', 'AllowPrefix' => false], + 'mi^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Mile', 'AllowPrefix' => false], + 'yd3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Yard', 'AllowPrefix' => false], + 'yd^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Yard', 'AllowPrefix' => false], + 'Nmi3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Nautical Mile', 'AllowPrefix' => false], + 'Nmi^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Nautical Mile', 'AllowPrefix' => false], + 'Pica3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false], + 'Pica^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false], + 'Picapt3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false], + 'Picapt^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false], + 'GRT' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Gross Registered Ton', 'AllowPrefix' => false], + 'regton' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Gross Registered Ton', 'AllowPrefix' => false], + 'MTON' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Measurement Ton (Freight Ton)', 'AllowPrefix' => false], + // Area + 'ha' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Hectare', 'AllowPrefix' => true], + 'uk_acre' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'International Acre', 'AllowPrefix' => false], + 'us_acre' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'US Survey/Statute Acre', 'AllowPrefix' => false], + 'ang2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Angstrom', 'AllowPrefix' => true], + 'ang^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Angstrom', 'AllowPrefix' => true], + 'ar' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Are', 'AllowPrefix' => true], + 'ft2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Feet', 'AllowPrefix' => false], + 'ft^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Feet', 'AllowPrefix' => false], + 'in2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Inches', 'AllowPrefix' => false], + 'in^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Inches', 'AllowPrefix' => false], + 'ly2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Light Years', 'AllowPrefix' => false], + 'ly^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Light Years', 'AllowPrefix' => false], + 'm2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Meters', 'AllowPrefix' => true], + 'm^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Meters', 'AllowPrefix' => true], + 'Morgen' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Morgen', 'AllowPrefix' => false], + 'mi2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Miles', 'AllowPrefix' => false], + 'mi^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Miles', 'AllowPrefix' => false], + 'Nmi2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Nautical Miles', 'AllowPrefix' => false], + 'Nmi^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Nautical Miles', 'AllowPrefix' => false], + 'Pica2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false], + 'Pica^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false], + 'Picapt2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false], + 'Picapt^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false], + 'yd2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Yards', 'AllowPrefix' => false], + 'yd^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Yards', 'AllowPrefix' => false], + // Information + 'byte' => ['Group' => self::CATEGORY_INFORMATION, 'UnitName' => 'Byte', 'AllowPrefix' => true], + 'bit' => ['Group' => self::CATEGORY_INFORMATION, 'UnitName' => 'Bit', 'AllowPrefix' => true], + // Speed + 'm/s' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per second', 'AllowPrefix' => true], + 'm/sec' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per second', 'AllowPrefix' => true], + 'm/h' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per hour', 'AllowPrefix' => true], + 'm/hr' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per hour', 'AllowPrefix' => true], + 'mph' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Miles per hour', 'AllowPrefix' => false], + 'admkn' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Admiralty Knot', 'AllowPrefix' => false], + 'kn' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Knot', 'AllowPrefix' => false], + ]; + + /** + * Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @var array + */ + private static array $conversionMultipliers = [ + 'Y' => ['multiplier' => 1E24, 'name' => 'yotta'], + 'Z' => ['multiplier' => 1E21, 'name' => 'zetta'], + 'E' => ['multiplier' => 1E18, 'name' => 'exa'], + 'P' => ['multiplier' => 1E15, 'name' => 'peta'], + 'T' => ['multiplier' => 1E12, 'name' => 'tera'], + 'G' => ['multiplier' => 1E9, 'name' => 'giga'], + 'M' => ['multiplier' => 1E6, 'name' => 'mega'], + 'k' => ['multiplier' => 1E3, 'name' => 'kilo'], + 'h' => ['multiplier' => 1E2, 'name' => 'hecto'], + 'e' => ['multiplier' => 1E1, 'name' => 'dekao'], + 'da' => ['multiplier' => 1E1, 'name' => 'dekao'], + 'd' => ['multiplier' => 1E-1, 'name' => 'deci'], + 'c' => ['multiplier' => 1E-2, 'name' => 'centi'], + 'm' => ['multiplier' => 1E-3, 'name' => 'milli'], + 'u' => ['multiplier' => 1E-6, 'name' => 'micro'], + 'n' => ['multiplier' => 1E-9, 'name' => 'nano'], + 'p' => ['multiplier' => 1E-12, 'name' => 'pico'], + 'f' => ['multiplier' => 1E-15, 'name' => 'femto'], + 'a' => ['multiplier' => 1E-18, 'name' => 'atto'], + 'z' => ['multiplier' => 1E-21, 'name' => 'zepto'], + 'y' => ['multiplier' => 1E-24, 'name' => 'yocto'], + ]; + + /** + * Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @var array + */ + private static array $binaryConversionMultipliers = [ + 'Yi' => ['multiplier' => 2 ** 80, 'name' => 'yobi'], + 'Zi' => ['multiplier' => 2 ** 70, 'name' => 'zebi'], + 'Ei' => ['multiplier' => 2 ** 60, 'name' => 'exbi'], + 'Pi' => ['multiplier' => 2 ** 50, 'name' => 'pebi'], + 'Ti' => ['multiplier' => 2 ** 40, 'name' => 'tebi'], + 'Gi' => ['multiplier' => 2 ** 30, 'name' => 'gibi'], + 'Mi' => ['multiplier' => 2 ** 20, 'name' => 'mebi'], + 'ki' => ['multiplier' => 2 ** 10, 'name' => 'kibi'], + ]; + + /** + * Details of the Units of measure conversion factors, organised by group. + * + * @var array> + */ + private static array $unitConversions = [ + // Conversion uses gram (g) as an intermediate unit + self::CATEGORY_WEIGHT_AND_MASS => [ + 'g' => 1.0, + 'sg' => 6.85217658567918E-05, + 'lbm' => 2.20462262184878E-03, + 'u' => 6.02214179421676E+23, + 'ozm' => 3.52739619495804E-02, + 'grain' => 1.54323583529414E+01, + 'cwt' => 2.20462262184878E-05, + 'shweight' => 2.20462262184878E-05, + 'uk_cwt' => 1.96841305522212E-05, + 'lcwt' => 1.96841305522212E-05, + 'hweight' => 1.96841305522212E-05, + 'stone' => 1.57473044417770E-04, + 'ton' => 1.10231131092439E-06, + 'uk_ton' => 9.84206527611061E-07, + 'LTON' => 9.84206527611061E-07, + 'brton' => 9.84206527611061E-07, + ], + // Conversion uses meter (m) as an intermediate unit + self::CATEGORY_DISTANCE => [ + 'm' => 1.0, + 'mi' => 6.21371192237334E-04, + 'Nmi' => 5.39956803455724E-04, + 'in' => 3.93700787401575E+01, + 'ft' => 3.28083989501312E+00, + 'yd' => 1.09361329833771E+00, + 'ang' => 1.0E+10, + 'ell' => 8.74890638670166E-01, + 'ly' => 1.05700083402462E-16, + 'parsec' => 3.24077928966473E-17, + 'pc' => 3.24077928966473E-17, + 'Pica' => 2.83464566929134E+03, + 'Picapt' => 2.83464566929134E+03, + 'pica' => 2.36220472440945E+02, + 'survey_mi' => 6.21369949494950E-04, + ], + // Conversion uses second (s) as an intermediate unit + self::CATEGORY_TIME => [ + 'yr' => 3.16880878140289E-08, + 'day' => 1.15740740740741E-05, + 'd' => 1.15740740740741E-05, + 'hr' => 2.77777777777778E-04, + 'mn' => 1.66666666666667E-02, + 'min' => 1.66666666666667E-02, + 'sec' => 1.0, + 's' => 1.0, + ], + // Conversion uses Pascal (Pa) as an intermediate unit + self::CATEGORY_PRESSURE => [ + 'Pa' => 1.0, + 'p' => 1.0, + 'atm' => 9.86923266716013E-06, + 'at' => 9.86923266716013E-06, + 'mmHg' => 7.50063755419211E-03, + 'psi' => 1.45037737730209E-04, + 'Torr' => 7.50061682704170E-03, + ], + // Conversion uses Newton (N) as an intermediate unit + self::CATEGORY_FORCE => [ + 'N' => 1.0, + 'dyn' => 1.0E+5, + 'dy' => 1.0E+5, + 'lbf' => 2.24808923655339E-01, + 'pond' => 1.01971621297793E+02, + ], + // Conversion uses Joule (J) as an intermediate unit + self::CATEGORY_ENERGY => [ + 'J' => 1.0, + 'e' => 9.99999519343231E+06, + 'c' => 2.39006249473467E-01, + 'cal' => 2.38846190642017E-01, + 'eV' => 6.24145700000000E+18, + 'ev' => 6.24145700000000E+18, + 'HPh' => 3.72506430801000E-07, + 'hh' => 3.72506430801000E-07, + 'Wh' => 2.77777916238711E-04, + 'wh' => 2.77777916238711E-04, + 'flb' => 2.37304222192651E+01, + 'BTU' => 9.47815067349015E-04, + 'btu' => 9.47815067349015E-04, + ], + // Conversion uses Horsepower (HP) as an intermediate unit + self::CATEGORY_POWER => [ + 'HP' => 1.0, + 'h' => 1.0, + 'W' => 7.45699871582270E+02, + 'w' => 7.45699871582270E+02, + 'PS' => 1.01386966542400E+00, + ], + // Conversion uses Tesla (T) as an intermediate unit + self::CATEGORY_MAGNETISM => [ + 'T' => 1.0, + 'ga' => 10000.0, + ], + // Conversion uses litre (l) as an intermediate unit + self::CATEGORY_VOLUME => [ + 'l' => 1.0, + 'L' => 1.0, + 'lt' => 1.0, + 'tsp' => 2.02884136211058E+02, + 'tspm' => 2.0E+02, + 'tbs' => 6.76280454036860E+01, + 'oz' => 3.38140227018430E+01, + 'cup' => 4.22675283773038E+00, + 'pt' => 2.11337641886519E+00, + 'us_pt' => 2.11337641886519E+00, + 'uk_pt' => 1.75975398639270E+00, + 'qt' => 1.05668820943259E+00, + 'uk_qt' => 8.79876993196351E-01, + 'gal' => 2.64172052358148E-01, + 'uk_gal' => 2.19969248299088E-01, + 'ang3' => 1.0E+27, + 'ang^3' => 1.0E+27, + 'barrel' => 6.28981077043211E-03, + 'bushel' => 2.83775932584017E-02, + 'in3' => 6.10237440947323E+01, + 'in^3' => 6.10237440947323E+01, + 'ft3' => 3.53146667214886E-02, + 'ft^3' => 3.53146667214886E-02, + 'ly3' => 1.18093498844171E-51, + 'ly^3' => 1.18093498844171E-51, + 'm3' => 1.0E-03, + 'm^3' => 1.0E-03, + 'mi3' => 2.39912758578928E-13, + 'mi^3' => 2.39912758578928E-13, + 'yd3' => 1.30795061931439E-03, + 'yd^3' => 1.30795061931439E-03, + 'Nmi3' => 1.57426214685811E-13, + 'Nmi^3' => 1.57426214685811E-13, + 'Pica3' => 2.27769904358706E+07, + 'Pica^3' => 2.27769904358706E+07, + 'Picapt3' => 2.27769904358706E+07, + 'Picapt^3' => 2.27769904358706E+07, + 'GRT' => 3.53146667214886E-04, + 'regton' => 3.53146667214886E-04, + 'MTON' => 8.82866668037215E-04, + ], + // Conversion uses hectare (ha) as an intermediate unit + self::CATEGORY_AREA => [ + 'ha' => 1.0, + 'uk_acre' => 2.47105381467165E+00, + 'us_acre' => 2.47104393046628E+00, + 'ang2' => 1.0E+24, + 'ang^2' => 1.0E+24, + 'ar' => 1.0E+02, + 'ft2' => 1.07639104167097E+05, + 'ft^2' => 1.07639104167097E+05, + 'in2' => 1.55000310000620E+07, + 'in^2' => 1.55000310000620E+07, + 'ly2' => 1.11725076312873E-28, + 'ly^2' => 1.11725076312873E-28, + 'm2' => 1.0E+04, + 'm^2' => 1.0E+04, + 'Morgen' => 4.0E+00, + 'mi2' => 3.86102158542446E-03, + 'mi^2' => 3.86102158542446E-03, + 'Nmi2' => 2.91553349598123E-03, + 'Nmi^2' => 2.91553349598123E-03, + 'Pica2' => 8.03521607043214E+10, + 'Pica^2' => 8.03521607043214E+10, + 'Picapt2' => 8.03521607043214E+10, + 'Picapt^2' => 8.03521607043214E+10, + 'yd2' => 1.19599004630108E+04, + 'yd^2' => 1.19599004630108E+04, + ], + // Conversion uses bit (bit) as an intermediate unit + self::CATEGORY_INFORMATION => [ + 'bit' => 1.0, + 'byte' => 0.125, + ], + // Conversion uses Meters per Second (m/s) as an intermediate unit + self::CATEGORY_SPEED => [ + 'm/s' => 1.0, + 'm/sec' => 1.0, + 'm/h' => 3.60E+03, + 'm/hr' => 3.60E+03, + 'mph' => 2.23693629205440E+00, + 'admkn' => 1.94260256941567E+00, + 'kn' => 1.94384449244060E+00, + ], + ]; + + /** + * getConversionGroups + * Returns a list of the different conversion groups for UOM conversions. + * + * @return string[] + */ + public static function getConversionCategories(): array + { + $conversionGroups = []; + foreach (self::$conversionUnits as $conversionUnit) { + $conversionGroups[] = $conversionUnit['Group']; + } + + return array_merge(array_unique($conversionGroups)); + } + + /** + * getConversionGroupUnits + * Returns an array of units of measure, for a specified conversion group, or for all groups. + * + * @param ?string $category The group whose units of measure you want to retrieve + * + * @return string[][] + */ + public static function getConversionCategoryUnits(?string $category = null): array + { + $conversionGroups = []; + foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { + if (($category === null) || ($conversionGroup['Group'] == $category)) { + $conversionGroups[$conversionGroup['Group']][] = $conversionUnit; + } + } + + return $conversionGroups; + } + + /** + * getConversionGroupUnitDetails. + * + * @param ?string $category The group whose units of measure you want to retrieve + * + * @return array>> + */ + public static function getConversionCategoryUnitDetails(?string $category = null): array + { + $conversionGroups = []; + foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { + if (($category === null) || ($conversionGroup['Group'] == $category)) { + $conversionGroups[$conversionGroup['Group']][] = [ + 'unit' => $conversionUnit, + 'description' => $conversionGroup['UnitName'], + ]; + } + } + + return $conversionGroups; + } + + /** + * getConversionMultipliers + * Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). + * + * @return array + */ + public static function getConversionMultipliers(): array + { + return self::$conversionMultipliers; + } + + /** + * getBinaryConversionMultipliers + * Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure in CONVERTUOM(). + * + * @return array + */ + public static function getBinaryConversionMultipliers(): array + { + return self::$binaryConversionMultipliers; + } + + /** + * CONVERT. + * + * Converts a number from one measurement system to another. + * For example, CONVERT can translate a table of distances in miles to a table of distances + * in kilometers. + * + * Excel Function: + * CONVERT(value,fromUOM,toUOM) + * + * @param array|float|int|string $value the value in fromUOM to convert + * Or can be an array of values + * @param string|string[] $fromUOM the units for value + * Or can be an array of values + * @param string|string[] $toUOM the units for the result + * Or can be an array of values + * + * @return float|mixed[]|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function CONVERT($value, $fromUOM, $toUOM) + { + if (is_array($value) || is_array($fromUOM) || is_array($toUOM)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $fromUOM, $toUOM); + } + + if (!is_numeric($value)) { + return ExcelError::VALUE(); + } + + try { + [$fromUOM, $fromCategory, $fromMultiplier] = self::getUOMDetails($fromUOM); + [$toUOM, $toCategory, $toMultiplier] = self::getUOMDetails($toUOM); + } catch (Exception) { + return ExcelError::NA(); + } + + if ($fromCategory !== $toCategory) { + return ExcelError::NA(); + } + + // @var float $value + $value *= $fromMultiplier; + + if (($fromUOM === $toUOM) && ($fromMultiplier === $toMultiplier)) { + // We've already factored $fromMultiplier into the value, so we need + // to reverse it again + return $value / $fromMultiplier; + } elseif ($fromUOM === $toUOM) { + return $value / $toMultiplier; + } elseif ($fromCategory === self::CATEGORY_TEMPERATURE) { + return self::convertTemperature($fromUOM, $toUOM, $value); + } + + $baseValue = $value * (1.0 / self::$unitConversions[$fromCategory][$fromUOM]); + + return ($baseValue * self::$unitConversions[$fromCategory][$toUOM]) / $toMultiplier; + } + + /** @return array{0: string, 1: string, 2: float} */ + private static function getUOMDetails(string $uom): array + { + if (isset(self::$conversionUnits[$uom])) { + $unitCategory = self::$conversionUnits[$uom]['Group']; + + return [$uom, $unitCategory, 1.0]; + } + + // Check 1-character standard metric multiplier prefixes + $multiplierType = substr($uom, 0, 1); + $uom = substr($uom, 1); + if (isset(self::$conversionUnits[$uom], self::$conversionMultipliers[$multiplierType])) { + if (self::$conversionUnits[$uom]['AllowPrefix'] === false) { + throw new Exception('Prefix not allowed for UoM'); + } + $unitCategory = self::$conversionUnits[$uom]['Group']; + + return [$uom, $unitCategory, self::$conversionMultipliers[$multiplierType]['multiplier']]; + } + + $multiplierType .= substr($uom, 0, 1); + $uom = substr($uom, 1); + + // Check 2-character standard metric multiplier prefixes + if (isset(self::$conversionUnits[$uom], self::$conversionMultipliers[$multiplierType])) { + if (self::$conversionUnits[$uom]['AllowPrefix'] === false) { + throw new Exception('Prefix not allowed for UoM'); + } + $unitCategory = self::$conversionUnits[$uom]['Group']; + + return [$uom, $unitCategory, self::$conversionMultipliers[$multiplierType]['multiplier']]; + } + + // Check 2-character binary multiplier prefixes + if (isset(self::$conversionUnits[$uom], self::$binaryConversionMultipliers[$multiplierType])) { + if (self::$conversionUnits[$uom]['AllowPrefix'] === false) { + throw new Exception('Prefix not allowed for UoM'); + } + $unitCategory = self::$conversionUnits[$uom]['Group']; + if ($unitCategory !== 'Information') { + throw new Exception('Binary Prefix is only allowed for Information UoM'); + } + + return [$uom, $unitCategory, self::$binaryConversionMultipliers[$multiplierType]['multiplier']]; + } + + throw new Exception('UoM Not Found'); + } + + protected static function convertTemperature(string $fromUOM, string $toUOM, float|int $value): float|int + { + $fromUOM = self::resolveTemperatureSynonyms($fromUOM); + $toUOM = self::resolveTemperatureSynonyms($toUOM); + + if ($fromUOM === $toUOM) { + return $value; + } + + // Convert to Kelvin + switch ($fromUOM) { + case 'F': + $value = ($value - 32) / 1.8 + 273.15; + + break; + case 'C': + $value += 273.15; + + break; + case 'Rank': + $value /= 1.8; + + break; + case 'Reau': + $value = $value * 1.25 + 273.15; + + break; + } + + // Convert from Kelvin + switch ($toUOM) { + case 'F': + $value = ($value - 273.15) * 1.8 + 32.00; + + break; + case 'C': + $value -= 273.15; + + break; + case 'Rank': + $value *= 1.8; + + break; + case 'Reau': + $value = ($value - 273.15) * 0.80000; + + break; + } + + return $value; + } + + private static function resolveTemperatureSynonyms(string $uom): string + { + return match ($uom) { + 'fah' => 'F', + 'cel' => 'C', + 'kel' => 'K', + default => $uom, + }; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php new file mode 100644 index 0000000..7e5118f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php @@ -0,0 +1,27 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function ERF(mixed $lower, mixed $upper = null): array|float|string + { + if (is_array($lower) || is_array($upper)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $lower, $upper); + } + + if (is_numeric($lower)) { + if ($upper === null) { + return self::erfValue($lower); + } + if (is_numeric($upper)) { + return self::erfValue($upper) - self::erfValue($lower); + } + } + + return ExcelError::VALUE(); + } + + /** + * ERFPRECISE. + * + * Returns the error function integrated between the lower and upper bound arguments. + * + * Excel Function: + * ERF.PRECISE(limit) + * + * @param mixed $limit Float bound for integrating ERF, other bound is zero + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function ERFPRECISE(mixed $limit) + { + if (is_array($limit)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $limit); + } + + return self::ERF($limit); + } + + private static function makeFloat(mixed $value): float + { + return is_numeric($value) ? ((float) $value) : 0.0; + } + + /** + * Method to calculate the erf value. + */ + public static function erfValue(float|int|string $value): float + { + $value = (float) $value; + if (abs($value) > 2.2) { + return 1 - self::makeFloat(ErfC::ERFC($value)); + } + $sum = $term = $value; + $xsqr = ($value * $value); + $j = 1; + do { + $term *= $xsqr / $j; + $sum -= $term / (2 * $j + 1); + ++$j; + $term *= $xsqr / $j; + $sum += $term / (2 * $j + 1); + ++$j; + if ($sum == 0.0) { + break; + } + } while (abs($term / $sum) > Functions::PRECISION); + + return self::TWO_SQRT_PI * $sum; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php new file mode 100644 index 0000000..db104bf --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php @@ -0,0 +1,77 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function ERFC(mixed $value) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if (is_numeric($value)) { + return self::erfcValue($value); + } + + return ExcelError::VALUE(); + } + + private const ONE_SQRT_PI = 0.564189583547756287; + + /** + * Method to calculate the erfc value. + */ + private static function erfcValue(float|int|string $value): float|int + { + $value = (float) $value; + if (abs($value) < 2.2) { + return 1 - Erf::erfValue($value); + } + if ($value < 0) { + return 2 - self::erfcValue(-$value); + } + $a = $n = 1; + $b = $c = $value; + $d = ($value * $value) + 0.5; + $q2 = $b / $d; + do { + $t = $a * $n + $b * $value; + $a = $b; + $b = $t; + $t = $c * $n + $d * $value; + $c = $d; + $d = $t; + $n += 0.5; + $q1 = $q2; + $q2 = $b / $d; + } while ((abs($q1 - $q2) / $q2) > Functions::PRECISION); + + return self::ONE_SQRT_PI * exp(-$value * $value) * $q2; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Exception.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Exception.php new file mode 100644 index 0000000..ea893d6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Exception.php @@ -0,0 +1,22 @@ +line = $line; + $e->file = $file; + + throw $e; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/ExceptionHandler.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/ExceptionHandler.php new file mode 100644 index 0000000..890da60 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/ExceptionHandler.php @@ -0,0 +1,24 @@ +getMessage(); + } + + $yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); + if (is_string($yearFracx)) { + return $yearFracx; + } + /** @var float $yearFrac */ + $yearFrac = $yearFracx; + + $amortiseCoeff = self::getAmortizationCoefficient($rate); + + $rate *= $amortiseCoeff; + $rate = (float) (string) $rate; // ugly way to avoid rounding problem + $fNRate = round($yearFrac * $rate * $cost, 0); + $cost -= $fNRate; + $fRest = $cost - $salvage; + + for ($n = 0; $n < $period; ++$n) { + $fNRate = round($rate * $cost, 0); + $fRest -= $fNRate; + + if ($fRest < 0.0) { + return match ($period - $n) { + 1 => round($cost * 0.5, 0), + default => 0.0, + }; + } + $cost -= $fNRate; + } + + return $fNRate; + } + + /** + * AMORLINC. + * + * Returns the depreciation for each accounting period. + * This function is provided for the French accounting system. If an asset is purchased in + * the middle of the accounting period, the prorated depreciation is taken into account. + * + * Excel Function: + * AMORLINC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) + * + * @param mixed $cost The cost of the asset as a float + * @param mixed $purchased Date of the purchase of the asset + * @param mixed $firstPeriod Date of the end of the first period + * @param mixed $salvage The salvage value at the end of the life of the asset + * @param mixed $period The period as a float + * @param mixed $rate Rate of depreciation as float + * @param mixed $basis Integer indicating the type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string (string containing the error type if there is an error) + */ + public static function AMORLINC( + mixed $cost, + mixed $purchased, + mixed $firstPeriod, + mixed $salvage, + mixed $period, + mixed $rate, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): string|float { + $cost = Functions::flattenSingleValue($cost); + $purchased = Functions::flattenSingleValue($purchased); + $firstPeriod = Functions::flattenSingleValue($firstPeriod); + $salvage = Functions::flattenSingleValue($salvage); + $period = Functions::flattenSingleValue($period); + $rate = Functions::flattenSingleValue($rate); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $cost = FinancialValidations::validateFloat($cost); + $purchased = FinancialValidations::validateDate($purchased); + $firstPeriod = FinancialValidations::validateDate($firstPeriod); + $salvage = FinancialValidations::validateFloat($salvage); + $period = FinancialValidations::validateFloat($period); + $rate = FinancialValidations::validateFloat($rate); + $basis = FinancialValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $fOneRate = $cost * $rate; + $fCostDelta = $cost - $salvage; + // Note, quirky variation for leap years on the YEARFRAC for this function + $purchasedYear = DateTimeExcel\DateParts::year($purchased); + $yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); + if (is_string($yearFracx)) { + return $yearFracx; + } + /** @var float $yearFrac */ + $yearFrac = $yearFracx; + + if ( + $basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL + && $yearFrac < 1 + ) { + $temp = Functions::scalar($purchasedYear); + if (is_int($temp) || is_string($temp)) { + if (DateTimeExcel\Helpers::isLeapYear($temp)) { + $yearFrac *= 365 / 366; + } + } + } + + $f0Rate = $yearFrac * $rate * $cost; + $nNumOfFullPeriods = (int) (($cost - $salvage - $f0Rate) / $fOneRate); + + if ($period == 0) { + return $f0Rate; + } elseif ($period <= $nNumOfFullPeriods) { + return $fOneRate; + } elseif ($period == ($nNumOfFullPeriods + 1)) { + return $fCostDelta - $fOneRate * $nNumOfFullPeriods - $f0Rate; + } + + return 0.0; + } + + private static function getAmortizationCoefficient(float $rate): float + { + // The depreciation coefficients are: + // Life of assets (1/rate) Depreciation coefficient + // Less than 3 years 1 + // Between 3 and 4 years 1.5 + // Between 5 and 6 years 2 + // More than 6 years 2.5 + $fUsePer = 1.0 / $rate; + + if ($fUsePer < 3.0) { + return 1.0; + } elseif ($fUsePer < 4.0) { + return 1.5; + } elseif ($fUsePer <= 6.0) { + return 2.0; + } + + return 2.5; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php new file mode 100644 index 0000000..f5719b6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php @@ -0,0 +1,41 @@ +getMessage(); + } + + return self::calculateFutureValue($rate, $numberOfPeriods, $payment, $presentValue, $type); + } + + /** + * PV. + * + * Returns the Present Value of a cash flow with constant payments and interest rate (annuities). + * + * @param mixed $rate Interest rate per period + * @param mixed $numberOfPeriods Number of periods as an integer + * @param mixed $payment Periodic payment (annuity) + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function presentValue( + mixed $rate, + mixed $numberOfPeriods, + mixed $payment = 0.0, + mixed $futureValue = 0.0, + mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ): string|float { + $rate = Functions::flattenSingleValue($rate); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $payment = Functions::flattenSingleValue($payment) ?? 0.0; + $futureValue = Functions::flattenSingleValue($futureValue) ?? 0.0; + $type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; + + try { + $rate = CashFlowValidations::validateRate($rate); + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $payment = CashFlowValidations::validateFloat($payment); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($numberOfPeriods < 0) { + return ExcelError::NAN(); + } + + return self::calculatePresentValue($rate, $numberOfPeriods, $payment, $futureValue, $type); + } + + /** + * NPER. + * + * Returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate. + * + * @param mixed $rate Interest rate per period + * @param mixed $payment Periodic payment (annuity) + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function periods( + mixed $rate, + mixed $payment, + mixed $presentValue, + mixed $futureValue = 0.0, + mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ) { + $rate = Functions::flattenSingleValue($rate); + $payment = Functions::flattenSingleValue($payment); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = Functions::flattenSingleValue($futureValue) ?? 0.0; + $type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; + + try { + $rate = CashFlowValidations::validateRate($rate); + $payment = CashFlowValidations::validateFloat($payment); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($payment == 0.0) { + return ExcelError::NAN(); + } + + return self::calculatePeriods($rate, $payment, $presentValue, $futureValue, $type); + } + + private static function calculateFutureValue( + float $rate, + int $numberOfPeriods, + float $payment, + float $presentValue, + int $type + ): float { + if ($rate != 0) { + return -$presentValue + * (1 + $rate) ** $numberOfPeriods - $payment * (1 + $rate * $type) * ((1 + $rate) ** $numberOfPeriods - 1) + / $rate; + } + + return -$presentValue - $payment * $numberOfPeriods; + } + + private static function calculatePresentValue( + float $rate, + int $numberOfPeriods, + float $payment, + float $futureValue, + int $type + ): float { + if ($rate != 0.0) { + return (-$payment * (1 + $rate * $type) + * (((1 + $rate) ** $numberOfPeriods - 1) / $rate) - $futureValue) / (1 + $rate) ** $numberOfPeriods; + } + + return -$futureValue - $payment * $numberOfPeriods; + } + + private static function calculatePeriods( + float $rate, + float $payment, + float $presentValue, + float $futureValue, + int $type + ): string|float { + if ($rate != 0.0) { + if ($presentValue == 0.0) { + return ExcelError::NAN(); + } + + return log(($payment * (1 + $rate * $type) / $rate - $futureValue) + / ($presentValue + $payment * (1 + $rate * $type) / $rate)) / log(1 + $rate); + } + + return (-$presentValue - $futureValue) / $payment; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php new file mode 100644 index 0000000..a19a61b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php @@ -0,0 +1,138 @@ +getMessage(); + } + + // Validate parameters + if ($start < 1 || $start > $end) { + return ExcelError::NAN(); + } + + // Calculate + $interest = 0; + for ($per = $start; $per <= $end; ++$per) { + $ipmt = Interest::payment($rate, $per, $periods, $presentValue, 0, $type); + if (is_string($ipmt)) { + return $ipmt; + } + + $interest += $ipmt; + } + + return $interest; + } + + /** + * CUMPRINC. + * + * Returns the cumulative principal paid on a loan between the start and end periods. + * + * Excel Function: + * CUMPRINC(rate,nper,pv,start,end[,type]) + * + * @param mixed $rate The Interest rate + * @param mixed $periods The total number of payment periods as an integer + * @param mixed $presentValue Present Value + * @param mixed $start The first period in the calculation. + * Payment periods are numbered beginning with 1. + * @param mixed $end the last period in the calculation + * @param mixed $type A number 0 or 1 and indicates when payments are due: + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. + */ + public static function principal( + mixed $rate, + mixed $periods, + mixed $presentValue, + mixed $start, + mixed $end, + mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ): string|float|int { + $rate = Functions::flattenSingleValue($rate); + $periods = Functions::flattenSingleValue($periods); + $presentValue = Functions::flattenSingleValue($presentValue); + $start = Functions::flattenSingleValue($start); + $end = Functions::flattenSingleValue($end); + $type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; + + try { + $rate = CashFlowValidations::validateRate($rate); + $periods = CashFlowValidations::validateInt($periods); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $start = CashFlowValidations::validateInt($start); + $end = CashFlowValidations::validateInt($end); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($start < 1 || $start > $end) { + return ExcelError::VALUE(); + } + + // Calculate + $principal = 0; + for ($per = $start; $per <= $end; ++$per) { + $ppmt = Payments::interestPayment($rate, $per, $periods, $presentValue, 0, $type); + if (is_string($ppmt)) { + return $ppmt; + } + + $principal += $ppmt; + } + + return $principal; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php new file mode 100644 index 0000000..33031cc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php @@ -0,0 +1,213 @@ +getMessage(); + } + + // Validate parameters + if ($period <= 0 || $period > $numberOfPeriods) { + return ExcelError::NAN(); + } + + // Calculate + $interestAndPrincipal = new InterestAndPrincipal( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue, + $type + ); + + return $interestAndPrincipal->interest(); + } + + /** + * ISPMT. + * + * Returns the interest payment for an investment based on an interest rate and a constant payment schedule. + * + * Excel Function: + * =ISPMT(interest_rate, period, number_payments, pv) + * + * @param mixed $interestRate is the interest rate for the investment + * @param mixed $period is the period to calculate the interest rate. It must be between 1 and number_payments. + * @param mixed $numberOfPeriods is the number of payments for the annuity + * @param mixed $principleRemaining is the loan amount or present value of the payments + */ + public static function schedulePayment(mixed $interestRate, mixed $period, mixed $numberOfPeriods, mixed $principleRemaining): string|float + { + $interestRate = Functions::flattenSingleValue($interestRate); + $period = Functions::flattenSingleValue($period); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $principleRemaining = Functions::flattenSingleValue($principleRemaining); + + try { + $interestRate = CashFlowValidations::validateRate($interestRate); + $period = CashFlowValidations::validateInt($period); + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $principleRemaining = CashFlowValidations::validateFloat($principleRemaining); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($period <= 0 || $period > $numberOfPeriods) { + return ExcelError::NAN(); + } + + // Return value + $returnValue = 0; + + // Calculate + $principlePayment = ($principleRemaining * 1.0) / ($numberOfPeriods * 1.0); + for ($i = 0; $i <= $period; ++$i) { + $returnValue = $interestRate * $principleRemaining * -1; + $principleRemaining -= $principlePayment; + // principle needs to be 0 after the last payment, don't let floating point screw it up + if ($i == $numberOfPeriods) { + $returnValue = 0.0; + } + } + + return $returnValue; + } + + /** + * RATE. + * + * Returns the interest rate per period of an annuity. + * RATE is calculated by iteration and can have zero or more solutions. + * If the successive results of RATE do not converge to within 0.0000001 after 20 iterations, + * RATE returns the #NUM! error value. + * + * Excel Function: + * RATE(nper,pmt,pv[,fv[,type[,guess]]]) + * + * @param mixed $numberOfPeriods The total number of payment periods in an annuity + * @param mixed $payment The payment made each period and cannot change over the life of the annuity. + * Typically, pmt includes principal and interest but no other fees or taxes. + * @param mixed $presentValue The present value - the total amount that a series of future payments is worth now + * @param mixed $futureValue The future value, or a cash balance you want to attain after the last payment is made. + * If fv is omitted, it is assumed to be 0 (the future value of a loan, + * for example, is 0). + * @param mixed $type A number 0 or 1 and indicates when payments are due: + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. + * @param mixed $guess Your guess for what the rate will be. + * If you omit guess, it is assumed to be 10 percent. + */ + public static function rate( + mixed $numberOfPeriods, + mixed $payment, + mixed $presentValue, + mixed $futureValue = 0.0, + mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD, + mixed $guess = 0.1 + ): string|float { + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $payment = Functions::flattenSingleValue($payment); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = Functions::flattenSingleValue($futureValue) ?? 0.0; + $type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; + $guess = Functions::flattenSingleValue($guess) ?? 0.1; + + try { + $numberOfPeriods = CashFlowValidations::validateFloat($numberOfPeriods); + $payment = CashFlowValidations::validateFloat($payment); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + $guess = CashFlowValidations::validateFloat($guess); + } catch (Exception $e) { + return $e->getMessage(); + } + + $rate = $guess; + // rest of code adapted from python/numpy + $close = false; + $iter = 0; + while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) { + $nextdiff = self::rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type); + if (!is_numeric($nextdiff)) { + break; + } + $rate1 = $rate - $nextdiff; + $close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION; + ++$iter; + $rate = $rate1; + } + + return $close ? $rate : ExcelError::NAN(); + } + + private static function rateNextGuess(float $rate, float $numberOfPeriods, float $payment, float $presentValue, float $futureValue, int $type): string|float + { + if ($rate == 0.0) { + return ExcelError::NAN(); + } + $tt1 = ($rate + 1) ** $numberOfPeriods; + $tt2 = ($rate + 1) ** ($numberOfPeriods - 1); + $numerator = $futureValue + $tt1 * $presentValue + $payment * ($tt1 - 1) * ($rate * $type + 1) / $rate; + $denominator = $numberOfPeriods * $tt2 * $presentValue - $payment * ($tt1 - 1) + * ($rate * $type + 1) / ($rate * $rate) + $numberOfPeriods + * $payment * $tt2 * ($rate * $type + 1) / $rate + $payment * ($tt1 - 1) * $type / $rate; + if ($denominator == 0) { + return ExcelError::NAN(); + } + + return $numerator / $denominator; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php new file mode 100644 index 0000000..ea9abb9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php @@ -0,0 +1,44 @@ +interest = $interest; + $this->principal = $principal; + } + + public function interest(): float + { + return $this->interest; + } + + public function principal(): float + { + return $this->principal; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php new file mode 100644 index 0000000..d16be44 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php @@ -0,0 +1,116 @@ +getMessage(); + } + + // Calculate + if ($interestRate != 0.0) { + return (-$futureValue - $presentValue * (1 + $interestRate) ** $numberOfPeriods) + / (1 + $interestRate * $type) / (((1 + $interestRate) ** $numberOfPeriods - 1) / $interestRate); + } + + return (-$presentValue - $futureValue) / $numberOfPeriods; + } + + /** + * PPMT. + * + * Returns the interest payment for a given period for an investment based on periodic, constant payments + * and a constant interest rate. + * + * @param mixed $interestRate Interest rate per period + * @param mixed $period Period for which we want to find the interest + * @param mixed $numberOfPeriods Number of periods + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function interestPayment( + mixed $interestRate, + mixed $period, + mixed $numberOfPeriods, + mixed $presentValue, + mixed $futureValue = 0, + mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD + ): string|float { + $interestRate = Functions::flattenSingleValue($interestRate); + $period = Functions::flattenSingleValue($period); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = Functions::flattenSingleValue($type) ?? FinancialConstants::PAYMENT_END_OF_PERIOD; + + try { + $interestRate = CashFlowValidations::validateRate($interestRate); + $period = CashFlowValidations::validateInt($period); + $numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + $type = CashFlowValidations::validatePeriodType($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($period <= 0 || $period > $numberOfPeriods) { + return ExcelError::NAN(); + } + + // Calculate + $interestAndPrincipal = new InterestAndPrincipal( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue, + $type + ); + + return $interestAndPrincipal->principal(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php new file mode 100644 index 0000000..ba3da6b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php @@ -0,0 +1,107 @@ +getMessage(); + } + + return $principal; + } + + /** + * PDURATION. + * + * Calculates the number of periods required for an investment to reach a specified value. + * + * @param mixed $rate Interest rate per period + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * + * @return float|string Result, or a string containing an error + */ + public static function periods(mixed $rate, mixed $presentValue, mixed $futureValue): string|float + { + $rate = Functions::flattenSingleValue($rate); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = Functions::flattenSingleValue($futureValue); + + try { + $rate = CashFlowValidations::validateRate($rate); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) { + return ExcelError::NAN(); + } + + return (log($futureValue) - log($presentValue)) / log(1 + $rate); + } + + /** + * RRI. + * + * Calculates the interest rate required for an investment to grow to a specified future value . + * + * @param mixed $periods The number of periods over which the investment is made, expect array|float + * @param mixed $presentValue Present Value, expect array|float + * @param mixed $futureValue Future Value, expect array|float + * + * @return float|string Result, or a string containing an error + */ + public static function interestRate(mixed $periods = 0.0, mixed $presentValue = 0.0, mixed $futureValue = 0.0): string|float + { + $periods = Functions::flattenSingleValue($periods); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = Functions::flattenSingleValue($futureValue); + + try { + $periods = CashFlowValidations::validateFloat($periods); + $presentValue = CashFlowValidations::validatePresentValue($presentValue); + $futureValue = CashFlowValidations::validateFutureValue($futureValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) { + return ExcelError::NAN(); + } + + return ($futureValue / $presentValue) ** (1 / $periods) - 1; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php new file mode 100644 index 0000000..53668d1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -0,0 +1,324 @@ + $values A series of cash flow payments, expecting float[] + * The series of values must contain at least one positive value & one negative value + * @param array $dates A series of payment dates + * The first payment date indicates the beginning of the schedule of payments + * All other dates must be later than this date, but they may occur in any order + * @param mixed $guess An optional guess at the expected answer + */ + public static function rate(mixed $values, $dates, mixed $guess = self::DEFAULT_GUESS): float|string + { + $rslt = self::xirrPart1($values, $dates); + /** @var array $dates */ + if ($rslt !== '') { + return $rslt; + } + + // create an initial range, with a root somewhere between 0 and guess + $guess = Functions::flattenSingleValue($guess) ?? self::DEFAULT_GUESS; + if (!is_numeric($guess)) { + return ExcelError::VALUE(); + } + $guess = ($guess + 0.0) ?: self::DEFAULT_GUESS; + $x1 = 0.0; + $x2 = $guess + 0.0; + $f1 = self::xnpvOrdered($x1, $values, $dates, false); + $f2 = self::xnpvOrdered($x2, $values, $dates, false); + $found = false; + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + if (!is_numeric($f1)) { + return $f1; + } + if (!is_numeric($f2)) { + return $f2; + } + $f1 = (float) $f1; + $f2 = (float) $f2; + if (($f1 * $f2) < 0.0) { + $found = true; + + break; + } elseif (abs($f1) < abs($f2)) { + $x1 += 1.6 * ($x1 - $x2); + $f1 = self::xnpvOrdered($x1, $values, $dates, false); + } else { + $x2 += 1.6 * ($x2 - $x1); + $f2 = self::xnpvOrdered($x2, $values, $dates, false); + } + } + if ($found) { + return self::xirrPart3($values, $dates, $x1, $x2); + } + + // Newton-Raphson didn't work - try bisection + $x1 = $guess - 0.5; + $x2 = $guess + 0.5; + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $f1 = self::xnpvOrdered($x1, $values, $dates, false, true); + $f2 = self::xnpvOrdered($x2, $values, $dates, false, true); + if (!is_numeric($f1) || !is_numeric($f2)) { + break; + } + if ($f1 * $f2 <= 0) { + $found = true; + + break; + } + $x1 -= 0.5; + $x2 += 0.5; + } + if ($found) { + /** @var array $dates */ + return self::xirrBisection($values, $dates, $x1, $x2); + } + + return ExcelError::NAN(); + } + + /** + * XNPV. + * + * Returns the net present value for a schedule of cash flows that is not necessarily periodic. + * To calculate the net present value for a series of cash flows that is periodic, use the NPV function. + * + * Excel Function: + * =XNPV(rate,values,dates) + * + * @param mixed $rate the discount rate to apply to the cash flows, expect array|float + * @param array $values A series of cash flows that corresponds to a schedule of payments in dates, expecting float[]. + * The first payment is optional and corresponds to a cost or payment that occurs + * at the beginning of the investment. + * If the first value is a cost or payment, it must be a negative value. + * All succeeding payments are discounted based on a 365-day year. + * The series of values must contain at least one positive value and one negative value. + * @param mixed $dates A schedule of payment dates that corresponds to the cash flow payments, expecting mixed[]. + * The first payment date indicates the beginning of the schedule of payments. + * All other dates must be later than this date, but they may occur in any order. + */ + public static function presentValue(mixed $rate, mixed $values, mixed $dates): float|string + { + return self::xnpvOrdered($rate, $values, $dates, true); + } + + private static function bothNegAndPos(bool $neg, bool $pos): bool + { + return $neg && $pos; + } + + /** @param array $values */ + private static function xirrPart1(mixed &$values, mixed &$dates): string + { + /** @var array */ + $temp = Functions::flattenArray($values); + $values = $temp; + $dates = Functions::flattenArray($dates); + $valuesIsArray = count($values) > 1; + $datesIsArray = count($dates) > 1; + if (!$valuesIsArray && !$datesIsArray) { + return ExcelError::NA(); + } + if (count($values) != count($dates)) { + return ExcelError::NAN(); + } + + $datesCount = count($dates); + for ($i = 0; $i < $datesCount; ++$i) { + try { + $dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + return self::xirrPart2($values); + } + + /** @param array $values */ + private static function xirrPart2(array &$values): string + { + $valCount = count($values); + $foundpos = false; + $foundneg = false; + for ($i = 0; $i < $valCount; ++$i) { + $fld = $values[$i]; + if (!is_numeric($fld)) { //* @phpstan-ignore-line + return ExcelError::VALUE(); + } elseif ($fld > 0) { + $foundpos = true; + } elseif ($fld < 0) { + $foundneg = true; + } + } + if (!self::bothNegAndPos($foundneg, $foundpos)) { + return ExcelError::NAN(); + } + + return ''; + } + + /** + * @param array $values + * @param array $dates + */ + private static function xirrPart3(array $values, array $dates, float $x1, float $x2): float|string + { + $f = self::xnpvOrdered($x1, $values, $dates, false); + if ($f < 0.0) { + $rtb = $x1; + $dx = $x2 - $x1; + } else { + $rtb = $x2; + $dx = $x1 - $x2; + } + + $rslt = ExcelError::VALUE(); + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $dx *= 0.5; + $x_mid = $rtb + $dx; + $f_mid = (float) self::xnpvOrdered($x_mid, $values, $dates, false); + if ($f_mid <= 0.0) { + $rtb = $x_mid; + } + if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { + $rslt = $x_mid; + + break; + } + } + + return $rslt; + } + + /** + * @param array $values + * @param array $dates + */ + private static function xirrBisection(array $values, array $dates, float $x1, float $x2): string|float + { + $rslt = ExcelError::NAN(); + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $rslt = ExcelError::NAN(); + $f1 = self::xnpvOrdered($x1, $values, $dates, false, true); + $f2 = self::xnpvOrdered($x2, $values, $dates, false, true); + if (!is_numeric($f1) || !is_numeric($f2)) { + break; + } + $f1 = (float) $f1; + $f2 = (float) $f2; + if (abs($f1) < self::FINANCIAL_PRECISION && abs($f2) < self::FINANCIAL_PRECISION) { + break; + } + if ($f1 * $f2 > 0) { + break; + } + $rslt = ($x1 + $x2) / 2; + $f3 = self::xnpvOrdered($rslt, $values, $dates, false, true); + if (!is_float($f3)) { + break; + } + if ($f3 * $f1 < 0) { + $x2 = $rslt; + } else { + $x1 = $rslt; + } + if (abs($f3) < self::FINANCIAL_PRECISION) { + break; + } + } + + return $rslt; + } + + /** @param array $values> */ + private static function xnpvOrdered(mixed $rate, mixed $values, mixed $dates, bool $ordered = true, bool $capAtNegative1 = false): float|string + { + $rate = Functions::flattenSingleValue($rate); + if (!is_numeric($rate)) { + return ExcelError::VALUE(); + } + $values = Functions::flattenArray($values); + $dates = Functions::flattenArray($dates); + $valCount = count($values); + + try { + self::validateXnpv($rate, $values, $dates); + if ($capAtNegative1 && $rate <= -1) { + $rate = -1.0 + 1.0E-10; + } + $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); + } catch (Exception $e) { + return $e->getMessage(); + } + + $xnpv = 0.0; + for ($i = 0; $i < $valCount; ++$i) { + if (!is_numeric($values[$i])) { + return ExcelError::VALUE(); + } + + try { + $datei = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); + } + if ($date0 > $datei) { + $dif = $ordered ? ExcelError::NAN() : -((int) DateTimeExcel\Difference::interval($datei, $date0, 'd')); + } else { + $dif = Functions::scalar(DateTimeExcel\Difference::interval($date0, $datei, 'd')); + } + if (!is_numeric($dif)) { + return StringHelper::convertToString($dif); + } + if ($rate <= -1.0) { + $xnpv += -abs($values[$i] + 0) / (-1 - $rate) ** ($dif / 365); + } else { + $xnpv += $values[$i] / (1 + $rate) ** ($dif / 365); + } + } + + return is_finite($xnpv) ? $xnpv : ExcelError::VALUE(); + } + + /** + * @param mixed[] $values + * @param mixed[] $dates + */ + private static function validateXnpv(mixed $rate, array $values, array $dates): void + { + if (!is_numeric($rate)) { + throw new Exception(ExcelError::VALUE()); + } + $valCount = count($values); + if ($valCount != count($dates)) { + throw new Exception(ExcelError::NAN()); + } + if (count($values) > 1 && ((min($values) > 0) || (max($values) < 0))) { + throw new Exception(ExcelError::NAN()); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php new file mode 100644 index 0000000..c5c3bcb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php @@ -0,0 +1,164 @@ + 0.0) { + return ExcelError::VALUE(); + } + + $f = self::presentValue($x1, $values); + if ($f < 0.0) { + $rtb = $x1; + $dx = $x2 - $x1; + } else { + $rtb = $x2; + $dx = $x1 - $x2; + } + + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $dx *= 0.5; + $x_mid = $rtb + $dx; + $f_mid = self::presentValue($x_mid, $values); + if ($f_mid <= 0.0) { + $rtb = $x_mid; + } + if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { + return $x_mid; + } + } + + return ExcelError::VALUE(); + } + + /** + * MIRR. + * + * Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both + * the cost of the investment and the interest received on reinvestment of cash. + * + * Excel Function: + * MIRR(values,finance_rate, reinvestment_rate) + * + * @param mixed $values An array or a reference to cells that contain a series of payments and + * income occurring at regular intervals. + * Payments are negative value, income is positive values. + * @param mixed $financeRate The interest rate you pay on the money used in the cash flows + * @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them + * + * @return float|string Result, or a string containing an error + */ + public static function modifiedRate(mixed $values, mixed $financeRate, mixed $reinvestmentRate): string|float + { + if (!is_array($values)) { + return ExcelError::DIV0(); + } + $values = Functions::flattenArray($values); + /** @var float */ + $financeRate = Functions::flattenSingleValue($financeRate); + /** @var float */ + $reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate); + $n = count($values); + + $rr = 1.0 + $reinvestmentRate; + $fr = 1.0 + $financeRate; + + $npvPos = $npvNeg = 0.0; + foreach ($values as $i => $v) { + /** @var float $v */ + if ($v >= 0) { + $npvPos += $v / $rr ** $i; + } else { + $npvNeg += $v / $fr ** $i; + } + } + + if ($npvNeg === 0.0 || $npvPos === 0.0) { + return ExcelError::DIV0(); + } + + $mirr = ((-$npvPos * $rr ** $n) + / ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0; + + return is_finite($mirr) ? $mirr : ExcelError::NAN(); + } + + /** + * NPV. + * + * Returns the Net Present Value of a cash flow series given a discount rate. + * + * @param array $args + */ + public static function presentValue(mixed $rate, ...$args): int|float + { + $returnValue = 0; + + /** @var float */ + $rate = Functions::flattenSingleValue($rate); + $aArgs = Functions::flattenArray($args); + + // Calculate + $countArgs = count($aArgs); + for ($i = 1; $i <= $countArgs; ++$i) { + // Is it a numeric value? + if (is_numeric($aArgs[$i - 1])) { + $returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i; + } + } + + return $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Constants.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Constants.php new file mode 100644 index 0000000..17740b0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Constants.php @@ -0,0 +1,19 @@ +getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); + if (is_string($daysPerYear)) { + return ExcelError::VALUE(); + } + $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); + + if ($basis === FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) { + return abs((float) DateTimeExcel\Days::between($prev, $settlement)); + } + + return (float) DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear; + } + + /** + * COUPDAYS. + * + * Returns the number of days in the coupon period that contains the settlement date. + * + * Excel Function: + * COUPDAYS(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + */ + public static function COUPDAYS( + mixed $settlement, + mixed $maturity, + mixed $frequency, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): string|int|float { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = FinancialValidations::validateFrequency($frequency); + $basis = FinancialValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + switch ($basis) { + case FinancialConstants::BASIS_DAYS_PER_YEAR_365: + // Actual/365 + return 365 / $frequency; + case FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL: + // Actual/actual + if ($frequency == FinancialConstants::FREQUENCY_ANNUAL) { + $daysPerYear = (int) Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); + + return $daysPerYear / $frequency; + } + $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); + $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); + + return $next - $prev; + default: + // US (NASD) 30/360, Actual/360 or European 30/360 + return 360 / $frequency; + } + } + + /** + * COUPDAYSNC. + * + * Returns the number of days from the settlement date to the next coupon date. + * + * Excel Function: + * COUPDAYSNC(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int) . + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + */ + public static function COUPDAYSNC( + mixed $settlement, + mixed $maturity, + mixed $frequency, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): string|float { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = FinancialValidations::validateFrequency($frequency); + $basis = FinancialValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + /** @var int $daysPerYear */ + $daysPerYear = Helpers::daysPerYear(Functions::Scalar(DateTimeExcel\DateParts::year($settlement)), $basis); + $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); + + if ($basis === FinancialConstants::BASIS_DAYS_PER_YEAR_NASD) { + $settlementDate = Date::excelToDateTimeObject($settlement); + $settlementEoM = Helpers::isLastDayOfMonth($settlementDate); + if ($settlementEoM) { + ++$settlement; + } + } + + return (float) DateTimeExcel\YearFrac::fraction($settlement, $next, $basis) * $daysPerYear; + } + + /** + * COUPNCD. + * + * Returns the next coupon date after the settlement date. + * + * Excel Function: + * COUPNCD(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Excel date/time serial value or error message + */ + public static function COUPNCD( + mixed $settlement, + mixed $maturity, + mixed $frequency, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): string|float { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = FinancialValidations::validateFrequency($frequency); + FinancialValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); + } + + /** + * COUPNUM. + * + * Returns the number of coupons payable between the settlement date and maturity date, + * rounded up to the nearest whole coupon. + * + * Excel Function: + * COUPNUM(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + */ + public static function COUPNUM( + mixed $settlement, + mixed $maturity, + mixed $frequency, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): string|int { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = FinancialValidations::validateFrequency($frequency); + FinancialValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $yearsBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction( + $settlement, + $maturity, + FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ); + + return (int) ceil((float) $yearsBetweenSettlementAndMaturity * $frequency); + } + + /** + * COUPPCD. + * + * Returns the previous coupon date before the settlement date. + * + * Excel Function: + * COUPPCD(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Excel date/time serial value or error message + */ + public static function COUPPCD( + mixed $settlement, + mixed $maturity, + mixed $frequency, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): string|float { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = FinancialValidations::validateFrequency($frequency); + FinancialValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); + } + + private static function monthsDiff(DateTime $result, int $months, string $plusOrMinus, int $day, bool $lastDayFlag): void + { + $result->setDate((int) $result->format('Y'), (int) $result->format('m'), 1); + $result->modify("$plusOrMinus $months months"); + $daysInMonth = (int) $result->format('t'); + $result->setDate((int) $result->format('Y'), (int) $result->format('m'), $lastDayFlag ? $daysInMonth : min($day, $daysInMonth)); + } + + private static function couponFirstPeriodDate(float $settlement, float $maturity, int $frequency, bool $next): float + { + $months = 12 / $frequency; + + $result = Date::excelToDateTimeObject($maturity); + $day = (int) $result->format('d'); + $lastDayFlag = Helpers::isLastDayOfMonth($result); + + while ($settlement < Date::PHPToExcel($result)) { + self::monthsDiff($result, $months, '-', $day, $lastDayFlag); + } + if ($next === true) { + self::monthsDiff($result, $months, '+', $day, $lastDayFlag); + } + + return (float) Date::PHPToExcel($result); + } + + private static function validateCouponPeriod(float $settlement, float $maturity): void + { + if ($settlement >= $maturity) { + throw new Exception(ExcelError::NAN()); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php new file mode 100644 index 0000000..6ef1899 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php @@ -0,0 +1,265 @@ +getMessage(); + } + + if ($cost === self::$zeroPointZero) { + return 0.0; + } + + // Set Fixed Depreciation Rate + $fixedDepreciationRate = 1 - ($salvage / $cost) ** (1 / $life); + $fixedDepreciationRate = round($fixedDepreciationRate, 3); + + // Loop through each period calculating the depreciation + // TODO Handle period value between 0 and 1 (e.g. 0.5) + $previousDepreciation = 0; + $depreciation = 0; + for ($per = 1; $per <= $period; ++$per) { + if ($per == 1) { + $depreciation = $cost * $fixedDepreciationRate * $month / 12; + } elseif ($per == ($life + 1)) { + $depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate * (12 - $month) / 12; + } else { + $depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate; + } + $previousDepreciation += $depreciation; + } + + return $depreciation; + } + + /** + * DDB. + * + * Returns the depreciation of an asset for a specified period using the + * double-declining balance method or some other method you specify. + * + * Excel Function: + * DDB(cost,salvage,life,period[,factor]) + * + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation. + * (Sometimes called the salvage value of the asset) + * @param mixed $life Number of periods over which the asset is depreciated. + * (Sometimes called the useful life of the asset) + * @param mixed $period The period for which you want to calculate the + * depreciation. Period must use the same units as life. + * @param mixed $factor The rate at which the balance declines. + * If factor is omitted, it is assumed to be 2 (the + * double-declining balance method). + */ + public static function DDB(mixed $cost, mixed $salvage, mixed $life, mixed $period, mixed $factor = 2.0): float|string + { + $cost = Functions::flattenSingleValue($cost); + $salvage = Functions::flattenSingleValue($salvage); + $life = Functions::flattenSingleValue($life); + $period = Functions::flattenSingleValue($period); + $factor = Functions::flattenSingleValue($factor); + + try { + $cost = self::validateCost($cost); + $salvage = self::validateSalvage($salvage); + $life = self::validateLife($life); + $period = self::validatePeriod($period); + $factor = self::validateFactor($factor); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($period > $life) { + return ExcelError::NAN(); + } + + // Loop through each period calculating the depreciation + // TODO Handling for fractional $period values + $previousDepreciation = 0; + $depreciation = 0; + for ($per = 1; $per <= $period; ++$per) { + $depreciation = min( + ($cost - $previousDepreciation) * ($factor / $life), + ($cost - $salvage - $previousDepreciation) + ); + $previousDepreciation += $depreciation; + } + + return $depreciation; + } + + /** + * SLN. + * + * Returns the straight-line depreciation of an asset for one period + * + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation + * @param mixed $life Number of periods over which the asset is depreciated + * + * @return float|string Result, or a string containing an error + */ + public static function SLN(mixed $cost, mixed $salvage, mixed $life): string|float + { + $cost = Functions::flattenSingleValue($cost); + $salvage = Functions::flattenSingleValue($salvage); + $life = Functions::flattenSingleValue($life); + + try { + $cost = self::validateCost($cost, true); + $salvage = self::validateSalvage($salvage, true); + $life = self::validateLife($life, true); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($life === self::$zeroPointZero) { + return ExcelError::DIV0(); + } + + return ($cost - $salvage) / $life; + } + + /** + * SYD. + * + * Returns the sum-of-years' digits depreciation of an asset for a specified period. + * + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation + * @param mixed $life Number of periods over which the asset is depreciated + * @param mixed $period Period + * + * @return float|string Result, or a string containing an error + */ + public static function SYD(mixed $cost, mixed $salvage, mixed $life, mixed $period): string|float + { + $cost = Functions::flattenSingleValue($cost); + $salvage = Functions::flattenSingleValue($salvage); + $life = Functions::flattenSingleValue($life); + $period = Functions::flattenSingleValue($period); + + try { + $cost = self::validateCost($cost, true); + $salvage = self::validateSalvage($salvage); + $life = self::validateLife($life); + $period = self::validatePeriod($period); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($period > $life) { + return ExcelError::NAN(); + } + + $syd = (($cost - $salvage) * ($life - $period + 1) * 2) / ($life * ($life + 1)); + + return $syd; + } + + private static function validateCost(mixed $cost, bool $negativeValueAllowed = false): float + { + $cost = FinancialValidations::validateFloat($cost); + if ($cost < 0.0 && $negativeValueAllowed === false) { + throw new Exception(ExcelError::NAN()); + } + + return $cost; + } + + private static function validateSalvage(mixed $salvage, bool $negativeValueAllowed = false): float + { + $salvage = FinancialValidations::validateFloat($salvage); + if ($salvage < 0.0 && $negativeValueAllowed === false) { + throw new Exception(ExcelError::NAN()); + } + + return $salvage; + } + + private static function validateLife(mixed $life, bool $negativeValueAllowed = false): float + { + $life = FinancialValidations::validateFloat($life); + if ($life < 0.0 && $negativeValueAllowed === false) { + throw new Exception(ExcelError::NAN()); + } + + return $life; + } + + private static function validatePeriod(mixed $period, bool $negativeValueAllowed = false): float + { + $period = FinancialValidations::validateFloat($period); + if ($period <= 0.0 && $negativeValueAllowed === false) { + throw new Exception(ExcelError::NAN()); + } + + return $period; + } + + private static function validateMonth(mixed $month): int + { + $month = FinancialValidations::validateInt($month); + if ($month < 1) { + throw new Exception(ExcelError::NAN()); + } + + return $month; + } + + private static function validateFactor(mixed $factor): float + { + $factor = FinancialValidations::validateFloat($factor); + if ($factor <= 0.0) { + throw new Exception(ExcelError::NAN()); + } + + return $factor; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Dollar.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Dollar.php new file mode 100644 index 0000000..3fc64b6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Dollar.php @@ -0,0 +1,131 @@ +|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function format(mixed $number, mixed $precision = 2) + { + return Format::DOLLAR($number, $precision); + } + + /** + * DOLLARDE. + * + * Converts a dollar price expressed as an integer part and a fraction + * part into a dollar price expressed as a decimal number. + * Fractional dollar numbers are sometimes used for security prices. + * + * Excel Function: + * DOLLARDE(fractional_dollar,fraction) + * + * @param mixed $fractionalDollar Fractional Dollar + * Or can be an array of values + * @param mixed $fraction Fraction + * Or can be an array of values + * + * @return array|float|string + */ + public static function decimal(mixed $fractionalDollar = null, mixed $fraction = 0): array|string|float + { + if (is_array($fractionalDollar) || is_array($fraction)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $fractionalDollar, $fraction); + } + + try { + $fractionalDollar = FinancialValidations::validateFloat( + Functions::flattenSingleValue($fractionalDollar) ?? 0.0 + ); + $fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Additional parameter validations + if ($fraction < 0) { + return ExcelError::NAN(); + } + if ($fraction == 0) { + return ExcelError::DIV0(); + } + + $dollars = ($fractionalDollar < 0) ? ceil($fractionalDollar) : floor($fractionalDollar); + $cents = fmod($fractionalDollar, 1.0); + $cents /= $fraction; + $cents *= 10 ** ceil(log10($fraction)); + + return $dollars + $cents; + } + + /** + * DOLLARFR. + * + * Converts a dollar price expressed as a decimal number into a dollar price + * expressed as a fraction. + * Fractional dollar numbers are sometimes used for security prices. + * + * Excel Function: + * DOLLARFR(decimal_dollar,fraction) + * + * @param mixed $decimalDollar Decimal Dollar + * Or can be an array of values + * @param mixed $fraction Fraction + * Or can be an array of values + * + * @return array|float|string + */ + public static function fractional(mixed $decimalDollar = null, mixed $fraction = 0): array|string|float + { + if (is_array($decimalDollar) || is_array($fraction)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $decimalDollar, $fraction); + } + + try { + $decimalDollar = FinancialValidations::validateFloat( + Functions::flattenSingleValue($decimalDollar) ?? 0.0 + ); + $fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Additional parameter validations + if ($fraction < 0) { + return ExcelError::NAN(); + } + if ($fraction == 0) { + return ExcelError::DIV0(); + } + + $dollars = ($decimalDollar < 0.0) ? ceil($decimalDollar) : floor($decimalDollar); + $cents = fmod($decimalDollar, 1); + $cents *= $fraction; + $cents *= 10 ** (-ceil(log10($fraction))); + + return $dollars + $cents; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php new file mode 100644 index 0000000..e596fc3 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php @@ -0,0 +1,122 @@ + 4)) { + throw new Exception(ExcelError::NAN()); + } + + return $basis; + } + + public static function validatePrice(mixed $price): float + { + $price = self::validateFloat($price); + if ($price < 0.0) { + throw new Exception(ExcelError::NAN()); + } + + return $price; + } + + public static function validateParValue(mixed $parValue): float + { + $parValue = self::validateFloat($parValue); + if ($parValue < 0.0) { + throw new Exception(ExcelError::NAN()); + } + + return $parValue; + } + + public static function validateYield(mixed $yield): float + { + $yield = self::validateFloat($yield); + if ($yield < 0.0) { + throw new Exception(ExcelError::NAN()); + } + + return $yield; + } + + public static function validateDiscount(mixed $discount): float + { + $discount = self::validateFloat($discount); + if ($discount <= 0.0) { + throw new Exception(ExcelError::NAN()); + } + + return $discount; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Helpers.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Helpers.php new file mode 100644 index 0000000..c983ecf --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Helpers.php @@ -0,0 +1,61 @@ +format('d') === $date->format('t'); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php new file mode 100644 index 0000000..2916df6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php @@ -0,0 +1,71 @@ +getMessage(); + } + + if ($nominalRate <= 0 || $periodsPerYear < 1) { + return ExcelError::NAN(); + } + + return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1; + } + + /** + * NOMINAL. + * + * Returns the nominal interest rate given the effective rate and the number of compounding payments per year. + * + * @param mixed $effectiveRate Effective interest rate as a float + * @param mixed $periodsPerYear Integer number of compounding payments per year + * + * @return float|string Result, or a string containing an error + */ + public static function nominal(mixed $effectiveRate = 0, mixed $periodsPerYear = 0): string|float + { + $effectiveRate = Functions::flattenSingleValue($effectiveRate); + $periodsPerYear = Functions::flattenSingleValue($periodsPerYear); + + try { + $effectiveRate = FinancialValidations::validateFloat($effectiveRate); + $periodsPerYear = FinancialValidations::validateInt($periodsPerYear); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($effectiveRate <= 0 || $periodsPerYear < 1) { + return ExcelError::NAN(); + } + + // Calculate + return $periodsPerYear * (($effectiveRate + 1) ** (1 / $periodsPerYear) - 1); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php new file mode 100644 index 0000000..5f4379d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php @@ -0,0 +1,146 @@ +getMessage(); + } + + $daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis)); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return StringHelper::convertToString($daysBetweenIssueAndSettlement); + } + $daysBetweenFirstInterestAndSettlement = Functions::scalar(YearFrac::fraction($firstInterest, $settlement, $basis)); + if (!is_numeric($daysBetweenFirstInterestAndSettlement)) { + // return date error + return StringHelper::convertToString($daysBetweenFirstInterestAndSettlement); + } + + return $parValue * $rate * $daysBetweenIssueAndSettlement; + } + + /** + * ACCRINTM. + * + * Returns the accrued interest for a security that pays interest at maturity. + * + * Excel Function: + * ACCRINTM(issue,settlement,rate[,par[,basis]]) + * + * @param mixed $issue The security's issue date + * @param mixed $settlement The security's settlement (or maturity) date + * @param mixed $rate The security's annual coupon rate + * @param mixed $parValue The security's par value. + * If you omit parValue, ACCRINT uses $1,000. + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function atMaturity( + mixed $issue, + mixed $settlement, + mixed $rate, + mixed $parValue = 1000, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ) { + $issue = Functions::flattenSingleValue($issue); + $settlement = Functions::flattenSingleValue($settlement); + $rate = Functions::flattenSingleValue($rate); + $parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $issue = SecurityValidations::validateIssueDate($issue); + $settlement = SecurityValidations::validateSettlementDate($settlement); + SecurityValidations::validateSecurityPeriod($issue, $settlement); + $rate = SecurityValidations::validateRate($rate); + $parValue = SecurityValidations::validateParValue($parValue); + $basis = SecurityValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis)); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return StringHelper::convertToString($daysBetweenIssueAndSettlement); + } + + return $parValue * $rate * $daysBetweenIssueAndSettlement; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php new file mode 100644 index 0000000..12c26c2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php @@ -0,0 +1,276 @@ +getMessage(); + } + + $dsc = (float) Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis); + $e = (float) Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis); + $n = (int) Coupons::COUPNUM($settlement, $maturity, $frequency, $basis); + $a = (float) Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis); + + $baseYF = 1.0 + ($yield / $frequency); + $rfp = 100 * ($rate / $frequency); + $de = $dsc / $e; + + $result = $redemption / $baseYF ** (--$n + $de); + for ($k = 0; $k <= $n; ++$k) { + $result += $rfp / ($baseYF ** ($k + $de)); + } + $result -= $rfp * ($a / $e); + + return $result; + } + + /** + * PRICEDISC. + * + * Returns the price per $100 face value of a discounted security. + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $discount The security's discount rate + * @param mixed $redemption The security's redemption value per $100 face value + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function priceDiscounted( + mixed $settlement, + mixed $maturity, + mixed $discount, + mixed $redemption, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ) { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $discount = Functions::flattenSingleValue($discount); + $redemption = Functions::flattenSingleValue($redemption); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = SecurityValidations::validateSettlementDate($settlement); + $maturity = SecurityValidations::validateMaturityDate($maturity); + SecurityValidations::validateSecurityPeriod($settlement, $maturity); + $discount = SecurityValidations::validateDiscount($discount); + $redemption = SecurityValidations::validateRedemption($redemption); + $basis = SecurityValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenSettlementAndMaturity); + } + + return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); + } + + /** + * PRICEMAT. + * + * Returns the price per $100 face value of a security that pays interest at maturity. + * + * @param mixed $settlement The security's settlement date. + * The security's settlement date is the date after the issue date when the + * security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $issue The security's issue date + * @param mixed $rate The security's interest rate at date of issue + * @param mixed $yield The security's annual yield + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function priceAtMaturity( + mixed $settlement, + mixed $maturity, + mixed $issue, + mixed $rate, + mixed $yield, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ) { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $issue = Functions::flattenSingleValue($issue); + $rate = Functions::flattenSingleValue($rate); + $yield = Functions::flattenSingleValue($yield); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = SecurityValidations::validateSettlementDate($settlement); + $maturity = SecurityValidations::validateMaturityDate($maturity); + SecurityValidations::validateSecurityPeriod($settlement, $maturity); + $issue = SecurityValidations::validateIssueDate($issue); + $rate = SecurityValidations::validateRate($rate); + $yield = SecurityValidations::validateYield($yield); + $basis = SecurityValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis)); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return StringHelper::convertToString($daysBetweenIssueAndSettlement); + } + $daysBetweenIssueAndSettlement *= $daysPerYear; + $daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis)); + if (!is_numeric($daysBetweenIssueAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenIssueAndMaturity); + } + $daysBetweenIssueAndMaturity *= $daysPerYear; + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenSettlementAndMaturity); + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) + / (1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) + - (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100); + } + + /** + * RECEIVED. + * + * Returns the amount received at maturity for a fully invested Security. + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $investment The amount invested in the security + * @param mixed $discount The security's discount rate + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function received( + mixed $settlement, + mixed $maturity, + mixed $investment, + mixed $discount, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ) { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $investment = Functions::flattenSingleValue($investment); + $discount = Functions::flattenSingleValue($discount); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = SecurityValidations::validateSettlementDate($settlement); + $maturity = SecurityValidations::validateMaturityDate($maturity); + SecurityValidations::validateSecurityPeriod($settlement, $maturity); + $investment = SecurityValidations::validateFloat($investment); + $discount = SecurityValidations::validateDiscount($discount); + $basis = SecurityValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($investment <= 0) { + return ExcelError::NAN(); + } + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString(Functions::scalar($daysBetweenSettlementAndMaturity)); + } + + return $investment / (1 - ($discount * $daysBetweenSettlementAndMaturity)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php new file mode 100644 index 0000000..4a120e2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php @@ -0,0 +1,131 @@ +getMessage(); + } + + if ($price <= 0.0) { + return ExcelError::NAN(); + } + + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenSettlementAndMaturity); + } + + return (1 - $price / $redemption) / $daysBetweenSettlementAndMaturity; + } + + /** + * INTRATE. + * + * Returns the interest rate for a fully invested security. + * + * Excel Function: + * INTRATE(settlement,maturity,investment,redemption[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $investment the amount invested in the security + * @param mixed $redemption the amount to be received at maturity + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + */ + public static function interest( + mixed $settlement, + mixed $maturity, + mixed $investment, + mixed $redemption, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ): float|string { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $investment = Functions::flattenSingleValue($investment); + $redemption = Functions::flattenSingleValue($redemption); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = SecurityValidations::validateSettlementDate($settlement); + $maturity = SecurityValidations::validateMaturityDate($maturity); + SecurityValidations::validateSecurityPeriod($settlement, $maturity); + $investment = SecurityValidations::validateFloat($investment); + $redemption = SecurityValidations::validateRedemption($redemption); + $basis = SecurityValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($investment <= 0) { + return ExcelError::NAN(); + } + + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenSettlementAndMaturity); + } + + return (($redemption / $investment) - 1) / ($daysBetweenSettlementAndMaturity); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php new file mode 100644 index 0000000..a0804cb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php @@ -0,0 +1,32 @@ += $maturity) { + throw new Exception(ExcelError::NAN()); + } + } + + public static function validateRedemption(mixed $redemption): float + { + $redemption = self::validateFloat($redemption); + if ($redemption <= 0.0) { + throw new Exception(ExcelError::NAN()); + } + + return $redemption; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php new file mode 100644 index 0000000..7ccb132 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php @@ -0,0 +1,150 @@ +getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenSettlementAndMaturity); + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return (($redemption - $price) / $price) * ($daysPerYear / $daysBetweenSettlementAndMaturity); + } + + /** + * YIELDMAT. + * + * Returns the annual yield of a security that pays interest at maturity. + * + * @param mixed $settlement The security's settlement date. + * The security's settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $issue The security's issue date + * @param mixed $rate The security's interest rate at date of issue + * @param mixed $price The security's price per $100 face value + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function yieldAtMaturity( + mixed $settlement, + mixed $maturity, + mixed $issue, + mixed $rate, + mixed $price, + mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD + ) { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $issue = Functions::flattenSingleValue($issue); + $rate = Functions::flattenSingleValue($rate); + $price = Functions::flattenSingleValue($price); + $basis = Functions::flattenSingleValue($basis) ?? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD; + + try { + $settlement = SecurityValidations::validateSettlementDate($settlement); + $maturity = SecurityValidations::validateMaturityDate($maturity); + SecurityValidations::validateSecurityPeriod($settlement, $maturity); + $issue = SecurityValidations::validateIssueDate($issue); + $rate = SecurityValidations::validateRate($rate); + $price = SecurityValidations::validatePrice($price); + $basis = SecurityValidations::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis)); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return StringHelper::convertToString($daysBetweenIssueAndSettlement); + } + $daysBetweenIssueAndSettlement *= $daysPerYear; + $daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis)); + if (!is_numeric($daysBetweenIssueAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenIssueAndMaturity); + } + $daysBetweenIssueAndMaturity *= $daysPerYear; + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return StringHelper::convertToString($daysBetweenSettlementAndMaturity); + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return ((1 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate) + - (($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) + / (($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) + * ($daysPerYear / $daysBetweenSettlementAndMaturity); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php new file mode 100644 index 0000000..699efcc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php @@ -0,0 +1,146 @@ +getMessage(); + } + + if ($discount <= 0) { + return ExcelError::NAN(); + } + + $daysBetweenSettlementAndMaturity = $maturity - $settlement; + $daysPerYear = Helpers::daysPerYear( + Functions::scalar(DateTimeExcel\DateParts::year($maturity)), + FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL + ); + + if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { + return ExcelError::NAN(); + } + + return (365 * $discount) / (360 - $discount * $daysBetweenSettlementAndMaturity); + } + + /** + * TBILLPRICE. + * + * Returns the price per $100 face value for a Treasury bill. + * + * @param mixed $settlement The Treasury bill's settlement date. + * The Treasury bill's settlement date is the date after the issue date + * when the Treasury bill is traded to the buyer. + * @param mixed $maturity The Treasury bill's maturity date. + * The maturity date is the date when the Treasury bill expires. + * @param mixed $discount The Treasury bill's discount rate + * + * @return float|string Result, or a string containing an error + */ + public static function price(mixed $settlement, mixed $maturity, mixed $discount): string|float + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $discount = Functions::flattenSingleValue($discount); + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + $discount = FinancialValidations::validateFloat($discount); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($discount <= 0) { + return ExcelError::NAN(); + } + + $daysBetweenSettlementAndMaturity = $maturity - $settlement; + $daysPerYear = Helpers::daysPerYear( + Functions::scalar(DateTimeExcel\DateParts::year($maturity)), + FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL + ); + + if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { + return ExcelError::NAN(); + } + + $price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360)); + if ($price < 0.0) { + return ExcelError::NAN(); + } + + return $price; + } + + /** + * TBILLYIELD. + * + * Returns the yield for a Treasury bill. + * + * @param mixed $settlement The Treasury bill's settlement date. + * The Treasury bill's settlement date is the date after the issue date when + * the Treasury bill is traded to the buyer. + * @param mixed $maturity The Treasury bill's maturity date. + * The maturity date is the date when the Treasury bill expires. + * @param float|string $price The Treasury bill's price per $100 face value + */ + public static function yield(mixed $settlement, mixed $maturity, $price): string|float + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $price = Functions::flattenSingleValue($price); + + try { + $settlement = FinancialValidations::validateSettlementDate($settlement); + $maturity = FinancialValidations::validateMaturityDate($maturity); + $price = FinancialValidations::validatePrice($price); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysBetweenSettlementAndMaturity = $maturity - $settlement; + $daysPerYear = Helpers::daysPerYear( + Functions::scalar(DateTimeExcel\DateParts::year($maturity)), + FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL + ); + + if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { + return ExcelError::NAN(); + } + + return ((100 - $price) / $price) * (360 / $daysBetweenSettlementAndMaturity); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FormulaParser.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FormulaParser.php new file mode 100644 index 0000000..945bcd6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -0,0 +1,604 @@ +<'; + const OPERATORS_POSTFIX = '%'; + + /** + * Formula. + */ + private string $formula; + + /** + * Tokens. + * + * @var FormulaToken[] + */ + private array $tokens = []; + + /** + * Create a new FormulaParser. + * + * @param ?string $formula Formula to parse + */ + public function __construct(?string $formula = '') + { + // Check parameters + if ($formula === null) { + throw new Exception('Invalid parameter passed: formula'); + } + + // Initialise values + $this->formula = trim($formula); + // Parse! + $this->parseToTokens(); + } + + /** + * Get Formula. + */ + public function getFormula(): string + { + return $this->formula; + } + + /** + * Get Token. + * + * @param int $id Token id + */ + public function getToken(int $id = 0): FormulaToken + { + if (isset($this->tokens[$id])) { + return $this->tokens[$id]; + } + + throw new Exception("Token with id $id does not exist."); + } + + /** + * Get Token count. + */ + public function getTokenCount(): int + { + return count($this->tokens); + } + + /** + * Get Tokens. + * + * @return FormulaToken[] + */ + public function getTokens(): array + { + return $this->tokens; + } + + /** + * Parse to tokens. + */ + private function parseToTokens(): void + { + // No attempt is made to verify formulas; assumes formulas are derived from Excel, where + // they can only exist if valid; stack overflows/underflows sunk as nulls without exceptions. + + // Check if the formula has a valid starting = + $formulaLength = strlen($this->formula); + if ($formulaLength < 2 || $this->formula[0] != '=') { + return; + } + + // Helper variables + $tokens1 = $tokens2 = $stack = []; + $inString = $inPath = $inRange = $inError = false; + $nextToken = null; + //$token = $previousToken = null; + + $index = 1; + $value = ''; + + $ERRORS = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A']; + $COMPARATORS_MULTI = ['>=', '<=', '<>']; + + while ($index < $formulaLength) { + // state-dependent character evaluation (order is important) + + // double-quoted strings + // embeds are doubled + // end marks token + if ($inString) { + if ($this->formula[$index] == self::QUOTE_DOUBLE) { + if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_DOUBLE)) { + $value .= self::QUOTE_DOUBLE; + ++$index; + } else { + $inString = false; + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_TEXT); + $value = ''; + } + } else { + $value .= $this->formula[$index]; + } + ++$index; + + continue; + } + + // single-quoted strings (links) + // embeds are double + // end does not mark a token + if ($inPath) { + if ($this->formula[$index] == self::QUOTE_SINGLE) { + if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_SINGLE)) { + $value .= self::QUOTE_SINGLE; + ++$index; + } else { + $inPath = false; + } + } else { + $value .= $this->formula[$index]; + } + ++$index; + + continue; + } + + // bracked strings (R1C1 range index or linked workbook name) + // no embeds (changed to "()" by Excel) + // end does not mark a token + if ($inRange) { + if ($this->formula[$index] == self::BRACKET_CLOSE) { + $inRange = false; + } + $value .= $this->formula[$index]; + ++$index; + + continue; + } + + // error values + // end marks a token, determined from absolute list of values + if ($inError) { + $value .= $this->formula[$index]; + ++$index; + if (in_array($value, $ERRORS)) { + $inError = false; + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_ERROR); + $value = ''; + } + + continue; + } + + // scientific notation check + if (str_contains(self::OPERATORS_SN, $this->formula[$index])) { + if (strlen($value) > 1) { + if (preg_match('/^[1-9]{1}(\.\d+)?E{1}$/', $this->formula[$index]) != 0) { + $value .= $this->formula[$index]; + ++$index; + + continue; + } + } + } + + // independent character evaluation (order not important) + + // establish state-dependent character evaluations + if ($this->formula[$index] == self::QUOTE_DOUBLE) { + if ($value !== '') { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + $inString = true; + ++$index; + + continue; + } + + if ($this->formula[$index] == self::QUOTE_SINGLE) { + if ($value !== '') { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + $inPath = true; + ++$index; + + continue; + } + + if ($this->formula[$index] == self::BRACKET_OPEN) { + $inRange = true; + $value .= self::BRACKET_OPEN; + ++$index; + + continue; + } + + if ($this->formula[$index] == self::ERROR_START) { + if ($value !== '') { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + $inError = true; + $value .= self::ERROR_START; + ++$index; + + continue; + } + + // mark start and end of arrays and array rows + if ($this->formula[$index] == self::BRACE_OPEN) { + if ($value !== '') { + // unexpected + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN); + $value = ''; + } + + $tmp = new FormulaToken('ARRAY', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + + $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + + ++$index; + + continue; + } + + if ($this->formula[$index] == self::SEMICOLON) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + /** @var FormulaToken $tmp */ + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + $tmp = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT); + $tokens1[] = $tmp; + + $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + + ++$index; + + continue; + } + + if ($this->formula[$index] == self::BRACE_CLOSE) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + /** @var FormulaToken $tmp */ + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + /** @var FormulaToken $tmp */ + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + ++$index; + + continue; + } + + // trim white-space + if ($this->formula[$index] == self::WHITESPACE) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken('', FormulaToken::TOKEN_TYPE_WHITESPACE); + ++$index; + while (($this->formula[$index] == self::WHITESPACE) && ($index < $formulaLength)) { + ++$index; + } + + continue; + } + + // multi-character comparators + if (($index + 2) <= $formulaLength) { + if (in_array(substr($this->formula, $index, 2), $COMPARATORS_MULTI)) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken(substr($this->formula, $index, 2), FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_LOGICAL); + $index += 2; + + continue; + } + } + + // standard infix operators + if (str_contains(self::OPERATORS_INFIX, $this->formula[$index])) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORINFIX); + ++$index; + + continue; + } + + // standard postfix operators (only one) + if (str_contains(self::OPERATORS_POSTFIX, $this->formula[$index])) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX); + ++$index; + + continue; + } + + // start subexpression or function + if ($this->formula[$index] == self::PAREN_OPEN) { + if ($value !== '') { + $tmp = new FormulaToken($value, FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + $value = ''; + } else { + $tmp = new FormulaToken('', FormulaToken::TOKEN_TYPE_SUBEXPRESSION, FormulaToken::TOKEN_SUBTYPE_START); + $tokens1[] = $tmp; + $stack[] = clone $tmp; + } + ++$index; + + continue; + } + + // function, subexpression, or array parameters, or operand unions + if ($this->formula[$index] == self::COMMA) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + /** @var FormulaToken $tmp */ + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $stack[] = $tmp; + + if ($tmp->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) { + $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_UNION); + } else { + $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT); + } + ++$index; + + continue; + } + + // stop subexpression + if ($this->formula[$index] == self::PAREN_CLOSE) { + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + $value = ''; + } + + /** @var FormulaToken $tmp */ + $tmp = array_pop($stack); + $tmp->setValue(''); + $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP); + $tokens1[] = $tmp; + + ++$index; + + continue; + } + + // token accumulation + $value .= $this->formula[$index]; + ++$index; + } + + // dump remaining accumulation + if ($value !== '') { + $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND); + } + + // move tokenList to new set, excluding unnecessary white-space tokens and converting necessary ones to intersections + $tokenCount = count($tokens1); + for ($i = 0; $i < $tokenCount; ++$i) { + $token = $tokens1[$i]; + $previousToken = $tokens1[$i - 1] ?? null; + $nextToken = $tokens1[$i + 1] ?? null; + + if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) { + $tokens2[] = $token; + + continue; + } + + if ($previousToken === null) { + continue; + } + + if ( + !( + (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) + ) { + continue; + } + + if ($nextToken === null) { + continue; + } + + if ( + !( + (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) + || (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) + || ($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) + ) { + continue; + } + + $tokens2[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_INTERSECTION); + } + + // move tokens to final list, switching infix "-" operators to prefix when appropriate, switching infix "+" operators + // to noop when appropriate, identifying operand and infix-operator subtypes, and pulling "@" from function names + $this->tokens = []; + + $tokenCount = count($tokens2); + for ($i = 0; $i < $tokenCount; ++$i) { + $token = $tokens2[$i]; + $previousToken = $tokens2[$i - 1] ?? null; + + if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') { + if ($i == 0) { + $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX); + } elseif ( + (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); + } else { + $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX); + } + + $this->tokens[] = $token; + + continue; + } + + if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '+') { + if ($i == 0) { + continue; + } elseif ( + (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || (($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) + && ($previousToken?->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) + || ($previousToken?->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND) + ) { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); + } else { + continue; + } + + $this->tokens[] = $token; + + continue; + } + + if ( + $token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX + && $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING + ) { + if (str_contains('<>=', substr($token->getValue(), 0, 1))) { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL); + } elseif ($token->getValue() == '&') { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_CONCATENATION); + } else { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH); + } + + $this->tokens[] = $token; + + continue; + } + + if ( + $token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND + && $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING + ) { + if (!is_numeric($token->getValue())) { + if (strtoupper($token->getValue()) == 'TRUE' || strtoupper($token->getValue()) == 'FALSE') { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL); + } else { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_RANGE); + } + } else { + $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_NUMBER); + } + + $this->tokens[] = $token; + + continue; + } + + if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) { + if ($token->getValue() !== '') { + if (str_starts_with($token->getValue(), '@')) { + $token->setValue(substr($token->getValue(), 1)); + } + } + } + + $this->tokens[] = $token; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FormulaToken.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FormulaToken.php new file mode 100644 index 0000000..cc7d48f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FormulaToken.php @@ -0,0 +1,131 @@ +value = $value; + $this->tokenType = $tokenType; + $this->tokenSubType = $tokenSubType; + } + + /** + * Get Value. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Set Value. + */ + public function setValue(string $value): void + { + $this->value = $value; + } + + /** + * Get Token Type (represented by TOKEN_TYPE_*). + */ + public function getTokenType(): string + { + return $this->tokenType; + } + + /** + * Set Token Type (represented by TOKEN_TYPE_*). + */ + public function setTokenType(string $value): void + { + $this->tokenType = $value; + } + + /** + * Get Token SubType (represented by TOKEN_SUBTYPE_*). + */ + public function getTokenSubType(): string + { + return $this->tokenSubType; + } + + /** + * Set Token SubType (represented by TOKEN_SUBTYPE_*). + */ + public function setTokenSubType(string $value): void + { + $this->tokenSubType = $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FunctionArray.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FunctionArray.php new file mode 100644 index 0000000..bc7f3a7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/FunctionArray.php @@ -0,0 +1,2683 @@ + + */ + protected static array $phpSpreadsheetFunctions = [ + 'ABS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Absolute::class, 'evaluate'], + 'argumentCount' => '1', + ], + 'ACCRINT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\AccruedInterest::class, 'periodic'], + 'argumentCount' => '4-8', + ], + 'ACCRINTM' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\AccruedInterest::class, 'atMaturity'], + 'argumentCount' => '3-5', + ], + 'ACOS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cosine::class, 'acos'], + 'argumentCount' => '1', + ], + 'ACOSH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cosine::class, 'acosh'], + 'argumentCount' => '1', + ], + 'ACOT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cotangent::class, 'acot'], + 'argumentCount' => '1', + ], + 'ACOTH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cotangent::class, 'acoth'], + 'argumentCount' => '1', + ], + 'ADDRESS' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Address::class, 'cell'], + 'argumentCount' => '2-5', + ], + 'AGGREGATE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3+', + ], + 'AMORDEGRC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Amortization::class, 'AMORDEGRC'], + 'argumentCount' => '6,7', + ], + 'AMORLINC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Amortization::class, 'AMORLINC'], + 'argumentCount' => '6,7', + ], + 'ANCHORARRAY' => [ + 'category' => Category::CATEGORY_MICROSOFT_INTERNAL, + 'functionCall' => [Internal\ExcelArrayPseudoFunctions::class, 'anchorArray'], + 'argumentCount' => '1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'AND' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Operations::class, 'logicalAnd'], + 'argumentCount' => '1+', + ], + 'ARABIC' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Arabic::class, 'evaluate'], + 'argumentCount' => '1', + ], + 'AREAS' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1', + ], + 'ARRAYTOTEXT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'fromArray'], + 'argumentCount' => '1,2', + ], + 'ASC' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1', + ], + 'ASIN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Sine::class, 'asin'], + 'argumentCount' => '1', + ], + 'ASINH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Sine::class, 'asinh'], + 'argumentCount' => '1', + ], + 'ATAN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Tangent::class, 'atan'], + 'argumentCount' => '1', + ], + 'ATAN2' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Tangent::class, 'atan2'], + 'argumentCount' => '2', + ], + 'ATANH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Tangent::class, 'atanh'], + 'argumentCount' => '1', + ], + 'AVEDEV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages::class, 'averageDeviations'], + 'argumentCount' => '1+', + ], + 'AVERAGE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages::class, 'average'], + 'argumentCount' => '1+', + ], + 'AVERAGEA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages::class, 'averageA'], + 'argumentCount' => '1+', + ], + 'AVERAGEIF' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIF'], + 'argumentCount' => '2,3', + ], + 'AVERAGEIFS' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIFS'], + 'argumentCount' => '3+', + ], + 'BAHTTEXT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Thai::class, 'getBahtText'], + 'argumentCount' => '1', + ], + 'BASE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Base::class, 'evaluate'], + 'argumentCount' => '2,3', + ], + 'BESSELI' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BesselI::class, 'BESSELI'], + 'argumentCount' => '2', + ], + 'BESSELJ' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BesselJ::class, 'BESSELJ'], + 'argumentCount' => '2', + ], + 'BESSELK' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BesselK::class, 'BESSELK'], + 'argumentCount' => '2', + ], + 'BESSELY' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BesselY::class, 'BESSELY'], + 'argumentCount' => '2', + ], + 'BETADIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Beta::class, 'distribution'], + 'argumentCount' => '3-5', + ], + 'BETA.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '4-6', + ], + 'BETAINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'], + 'argumentCount' => '3-5', + ], + 'BETA.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'], + 'argumentCount' => '3-5', + ], + 'BIN2DEC' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertBinary::class, 'toDecimal'], + 'argumentCount' => '1', + ], + 'BIN2HEX' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertBinary::class, 'toHex'], + 'argumentCount' => '1,2', + ], + 'BIN2OCT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertBinary::class, 'toOctal'], + 'argumentCount' => '1,2', + ], + 'BINOMDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'], + 'argumentCount' => '4', + ], + 'BINOM.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'], + 'argumentCount' => '4', + ], + 'BINOM.DIST.RANGE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Binomial::class, 'range'], + 'argumentCount' => '3,4', + ], + 'BINOM.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'], + 'argumentCount' => '3', + ], + 'BITAND' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BitWise::class, 'BITAND'], + 'argumentCount' => '2', + ], + 'BITOR' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BitWise::class, 'BITOR'], + 'argumentCount' => '2', + ], + 'BITXOR' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BitWise::class, 'BITXOR'], + 'argumentCount' => '2', + ], + 'BITLSHIFT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BitWise::class, 'BITLSHIFT'], + 'argumentCount' => '2', + ], + 'BITRSHIFT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\BitWise::class, 'BITRSHIFT'], + 'argumentCount' => '2', + ], + 'BYCOL' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'BYROW' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'CEILING' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Ceiling::class, 'ceiling'], + 'argumentCount' => '1-2', // 2 for Excel, 1-2 for Ods/Gnumeric + ], + 'CEILING.MATH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Ceiling::class, 'math'], + 'argumentCount' => '1-3', + ], + // pseudo-function to help with Ods + 'CEILING.ODS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Ceiling::class, 'mathOds'], + 'argumentCount' => '1-3', + ], + 'CEILING.PRECISE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Ceiling::class, 'precise'], + 'argumentCount' => '1,2', + ], + // pseudo-function implemented in Ods + 'CEILING.XCL' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Ceiling::class, 'ceiling'], + 'argumentCount' => '2', + ], + 'CELL' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1,2', + ], + 'CHAR' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CharacterConvert::class, 'character'], + 'argumentCount' => '1', + ], + 'CHIDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'], + 'argumentCount' => '2', + ], + 'CHISQ.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionLeftTail'], + 'argumentCount' => '3', + ], + 'CHISQ.DIST.RT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'], + 'argumentCount' => '2', + ], + 'CHIINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'], + 'argumentCount' => '2', + ], + 'CHISQ.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseLeftTail'], + 'argumentCount' => '2', + ], + 'CHISQ.INV.RT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'], + 'argumentCount' => '2', + ], + 'CHITEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'], + 'argumentCount' => '2', + ], + 'CHISQ.TEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'], + 'argumentCount' => '2', + ], + 'CHOOSE' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Selection::class, 'CHOOSE'], + 'argumentCount' => '2+', + ], + 'CHOOSECOLS' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'chooseCols'], + 'argumentCount' => '2+', + ], + 'CHOOSEROWS' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'chooseRows'], + 'argumentCount' => '2+', + ], + 'CLEAN' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Trim::class, 'nonPrintable'], + 'argumentCount' => '1', + ], + 'CODE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CharacterConvert::class, 'code'], + 'argumentCount' => '1', + ], + 'COLUMN' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMN'], + 'argumentCount' => '-1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'COLUMNS' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMNS'], + 'argumentCount' => '1', + ], + 'COMBIN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Combinations::class, 'withoutRepetition'], + 'argumentCount' => '2', + ], + 'COMBINA' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Combinations::class, 'withRepetition'], + 'argumentCount' => '2', + ], + 'COMPLEX' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Complex::class, 'COMPLEX'], + 'argumentCount' => '2,3', + ], + 'CONCAT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'], + 'argumentCount' => '1+', + ], + 'CONCATENATE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'], + 'argumentCount' => '1+', + ], + 'CONFIDENCE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'], + 'argumentCount' => '3', + ], + 'CONFIDENCE.NORM' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'], + 'argumentCount' => '3', + ], + 'CONFIDENCE.T' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'CONVERT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertUOM::class, 'CONVERT'], + 'argumentCount' => '3', + ], + 'CORREL' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'CORREL'], + 'argumentCount' => '2', + ], + 'COS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cosine::class, 'cos'], + 'argumentCount' => '1', + ], + 'COSH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cosine::class, 'cosh'], + 'argumentCount' => '1', + ], + 'COT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cotangent::class, 'cot'], + 'argumentCount' => '1', + ], + 'COTH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cotangent::class, 'coth'], + 'argumentCount' => '1', + ], + 'COUNT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Counts::class, 'COUNT'], + 'argumentCount' => '1+', + ], + 'COUNTA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Counts::class, 'COUNTA'], + 'argumentCount' => '1+', + ], + 'COUNTBLANK' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Counts::class, 'COUNTBLANK'], + 'argumentCount' => '1', + ], + 'COUNTIF' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Conditional::class, 'COUNTIF'], + 'argumentCount' => '2', + ], + 'COUNTIFS' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Conditional::class, 'COUNTIFS'], + 'argumentCount' => '2+', + ], + 'COUPDAYBS' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Coupons::class, 'COUPDAYBS'], + 'argumentCount' => '3,4', + ], + 'COUPDAYS' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Coupons::class, 'COUPDAYS'], + 'argumentCount' => '3,4', + ], + 'COUPDAYSNC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Coupons::class, 'COUPDAYSNC'], + 'argumentCount' => '3,4', + ], + 'COUPNCD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Coupons::class, 'COUPNCD'], + 'argumentCount' => '3,4', + ], + 'COUPNUM' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Coupons::class, 'COUPNUM'], + 'argumentCount' => '3,4', + ], + 'COUPPCD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Coupons::class, 'COUPPCD'], + 'argumentCount' => '3,4', + ], + 'COVAR' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'COVAR'], + 'argumentCount' => '2', + ], + 'COVARIANCE.P' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'COVAR'], + 'argumentCount' => '2', + ], + 'COVARIANCE.S' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'CRITBINOM' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'], + 'argumentCount' => '3', + ], + 'CSC' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cosecant::class, 'csc'], + 'argumentCount' => '1', + ], + 'CSCH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Cosecant::class, 'csch'], + 'argumentCount' => '1', + ], + 'CUBEKPIMEMBER' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUBEMEMBER' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUBEMEMBERPROPERTY' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUBERANKEDMEMBER' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUBESET' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUBESETCOUNT' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUBEVALUE' => [ + 'category' => Category::CATEGORY_CUBE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'CUMIPMT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'interest'], + 'argumentCount' => '6', + ], + 'CUMPRINC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'principal'], + 'argumentCount' => '6', + ], + 'DATE' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Date::class, 'fromYMD'], + 'argumentCount' => '3', + ], + 'DATEDIF' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Difference::class, 'interval'], + 'argumentCount' => '2,3', + ], + 'DATESTRING' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'DATEVALUE' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\DateValue::class, 'fromString'], + 'argumentCount' => '1', + ], + 'DAVERAGE' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DAverage::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DAY' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\DateParts::class, 'day'], + 'argumentCount' => '1', + ], + 'DAYS' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Days::class, 'between'], + 'argumentCount' => '2', + ], + 'DAYS360' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Days360::class, 'between'], + 'argumentCount' => '2,3', + ], + 'DB' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Depreciation::class, 'DB'], + 'argumentCount' => '4,5', + ], + 'DBCS' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1', + ], + 'DCOUNT' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DCount::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DCOUNTA' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DCountA::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DDB' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Depreciation::class, 'DDB'], + 'argumentCount' => '4,5', + ], + 'DEC2BIN' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertDecimal::class, 'toBinary'], + 'argumentCount' => '1,2', + ], + 'DEC2HEX' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertDecimal::class, 'toHex'], + 'argumentCount' => '1,2', + ], + 'DEC2OCT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertDecimal::class, 'toOctal'], + 'argumentCount' => '1,2', + ], + 'DECIMAL' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'DEGREES' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Angle::class, 'toDegrees'], + 'argumentCount' => '1', + ], + 'DELTA' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Compare::class, 'DELTA'], + 'argumentCount' => '1,2', + ], + 'DEVSQ' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Deviations::class, 'sumSquares'], + 'argumentCount' => '1+', + ], + 'DGET' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DGet::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DISC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Rates::class, 'discount'], + 'argumentCount' => '4,5', + ], + 'DMAX' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DMax::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DMIN' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DMin::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DOLLAR' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Format::class, 'DOLLAR'], + 'argumentCount' => '1,2', + ], + 'DOLLARDE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Dollar::class, 'decimal'], + 'argumentCount' => '2', + ], + 'DOLLARFR' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Dollar::class, 'fractional'], + 'argumentCount' => '2', + ], + 'DPRODUCT' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DProduct::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DROP' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'drop'], + 'argumentCount' => '2-3', + ], + 'DSTDEV' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DStDev::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DSTDEVP' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DStDevP::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DSUM' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DSum::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DURATION' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '5,6', + ], + 'DVAR' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DVar::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'DVARP' => [ + 'category' => Category::CATEGORY_DATABASE, + 'functionCall' => [Database\DVarP::class, 'evaluate'], + 'argumentCount' => '3', + ], + 'ECMA.CEILING' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1,2', + ], + 'EDATE' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Month::class, 'adjust'], + 'argumentCount' => '2', + ], + 'EFFECT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\InterestRate::class, 'effective'], + 'argumentCount' => '2', + ], + 'ENCODEURL' => [ + 'category' => Category::CATEGORY_WEB, + 'functionCall' => [Web\Service::class, 'urlEncode'], + 'argumentCount' => '1', + ], + 'EOMONTH' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Month::class, 'lastDay'], + 'argumentCount' => '2', + ], + 'ERF' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Erf::class, 'ERF'], + 'argumentCount' => '1,2', + ], + 'ERF.PRECISE' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Erf::class, 'ERFPRECISE'], + 'argumentCount' => '1', + ], + 'ERFC' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ErfC::class, 'ERFC'], + 'argumentCount' => '1', + ], + 'ERFC.PRECISE' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ErfC::class, 'ERFC'], + 'argumentCount' => '1', + ], + 'ERROR.TYPE' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\ExcelError::class, 'type'], + 'argumentCount' => '1', + ], + 'EVEN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Round::class, 'even'], + 'argumentCount' => '1', + ], + 'EXACT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'exact'], + 'argumentCount' => '2', + ], + 'EXP' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Exp::class, 'evaluate'], + 'argumentCount' => '1', + ], + 'EXPAND' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'expand'], + 'argumentCount' => '2-4', + ], + 'EXPONDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'], + 'argumentCount' => '3', + ], + 'EXPON.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'], + 'argumentCount' => '3', + ], + 'FACT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Factorial::class, 'fact'], + 'argumentCount' => '1', + ], + 'FACTDOUBLE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Factorial::class, 'factDouble'], + 'argumentCount' => '1', + ], + 'FALSE' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Boolean::class, 'FALSE'], + 'argumentCount' => '0', + ], + 'FDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'F.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\F::class, 'distribution'], + 'argumentCount' => '4', + ], + 'F.DIST.RT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'FILTER' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Filter::class, 'filter'], + 'argumentCount' => '2-3', + ], + 'FILTERXML' => [ + 'category' => Category::CATEGORY_WEB, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'FIND' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Search::class, 'sensitive'], + 'argumentCount' => '2,3', + ], + 'FINDB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Search::class, 'sensitive'], + 'argumentCount' => '2,3', + ], + 'FINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'F.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'F.INV.RT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'FISHER' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Fisher::class, 'distribution'], + 'argumentCount' => '1', + ], + 'FISHERINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Fisher::class, 'inverse'], + 'argumentCount' => '1', + ], + 'FIXED' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Format::class, 'FIXEDFORMAT'], + 'argumentCount' => '1-3', + ], + 'FLOOR' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Floor::class, 'floor'], + 'argumentCount' => '1-2', // Excel requires 2, Ods/Gnumeric 1-2 + ], + 'FLOOR.MATH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Floor::class, 'math'], + 'argumentCount' => '1-3', + ], + // pseudo-function to help with Ods + 'FLOOR.ODS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Floor::class, 'mathOds'], + 'argumentCount' => '1-3', + ], + 'FLOOR.PRECISE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Floor::class, 'precise'], + 'argumentCount' => '1-2', + ], + // pseudo-function implemented in Ods + 'FLOOR.XCL' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Floor::class, 'floor'], + 'argumentCount' => '2', + ], + 'FORECAST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'FORECAST'], + 'argumentCount' => '3', + ], + 'FORECAST.ETS' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3-6', + ], + 'FORECAST.ETS.CONFINT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3-6', + ], + 'FORECAST.ETS.SEASONALITY' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-4', + ], + 'FORECAST.ETS.STAT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3-6', + ], + 'FORECAST.LINEAR' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'FORECAST'], + 'argumentCount' => '3', + ], + 'FORMULATEXT' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Formula::class, 'text'], + 'argumentCount' => '1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'FREQUENCY' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'FTEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'F.TEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'FV' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'futureValue'], + 'argumentCount' => '3-5', + ], + 'FVSCHEDULE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Single::class, 'futureValue'], + 'argumentCount' => '2', + ], + 'GAMMA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'gamma'], + 'argumentCount' => '1', + ], + 'GAMMADIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'], + 'argumentCount' => '4', + ], + 'GAMMA.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'], + 'argumentCount' => '4', + ], + 'GAMMAINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'], + 'argumentCount' => '3', + ], + 'GAMMA.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'], + 'argumentCount' => '3', + ], + 'GAMMALN' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'], + 'argumentCount' => '1', + ], + 'GAMMALN.PRECISE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'], + 'argumentCount' => '1', + ], + 'GAUSS' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'gauss'], + 'argumentCount' => '1', + ], + 'GCD' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Gcd::class, 'evaluate'], + 'argumentCount' => '1+', + ], + 'GEOMEAN' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages\Mean::class, 'geometric'], + 'argumentCount' => '1+', + ], + 'GESTEP' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Compare::class, 'GESTEP'], + 'argumentCount' => '1,2', + ], + 'GETPIVOTDATA' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2+', + ], + 'GROUPBY' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3-7', + ], + 'GROWTH' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'GROWTH'], + 'argumentCount' => '1-4', + ], + 'HARMEAN' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages\Mean::class, 'harmonic'], + 'argumentCount' => '1+', + ], + 'HEX2BIN' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertHex::class, 'toBinary'], + 'argumentCount' => '1,2', + ], + 'HEX2DEC' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertHex::class, 'toDecimal'], + 'argumentCount' => '1', + ], + 'HEX2OCT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertHex::class, 'toOctal'], + 'argumentCount' => '1,2', + ], + 'HLOOKUP' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\HLookup::class, 'lookup'], + 'argumentCount' => '3,4', + ], + 'HOUR' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\TimeParts::class, 'hour'], + 'argumentCount' => '1', + ], + 'HSTACK' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Hstack::class, 'hstack'], + 'argumentCount' => '1+', + ], + 'HYPERLINK' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Hyperlink::class, 'set'], + 'argumentCount' => '1,2', + 'passCellReference' => true, + ], + 'HYPGEOMDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\HyperGeometric::class, 'distribution'], + 'argumentCount' => '4', + ], + 'HYPGEOM.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '5', + ], + 'IF' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Conditional::class, 'statementIf'], + 'argumentCount' => '2-3', + ], + 'IFERROR' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Conditional::class, 'IFERROR'], + 'argumentCount' => '2', + ], + 'IFNA' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Conditional::class, 'IFNA'], + 'argumentCount' => '2', + ], + 'IFS' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Conditional::class, 'IFS'], + 'argumentCount' => '2+', + ], + 'IMABS' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMABS'], + 'argumentCount' => '1', + ], + 'IMAGINARY' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Complex::class, 'IMAGINARY'], + 'argumentCount' => '1', + ], + 'IMARGUMENT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMARGUMENT'], + 'argumentCount' => '1', + ], + 'IMCONJUGATE' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCONJUGATE'], + 'argumentCount' => '1', + ], + 'IMCOS' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOS'], + 'argumentCount' => '1', + ], + 'IMCOSH' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOSH'], + 'argumentCount' => '1', + ], + 'IMCOT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOT'], + 'argumentCount' => '1', + ], + 'IMCSC' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSC'], + 'argumentCount' => '1', + ], + 'IMCSCH' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSCH'], + 'argumentCount' => '1', + ], + 'IMDIV' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexOperations::class, 'IMDIV'], + 'argumentCount' => '2', + ], + 'IMEXP' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMEXP'], + 'argumentCount' => '1', + ], + 'IMLN' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLN'], + 'argumentCount' => '1', + ], + 'IMLOG10' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG10'], + 'argumentCount' => '1', + ], + 'IMLOG2' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG2'], + 'argumentCount' => '1', + ], + 'IMPOWER' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMPOWER'], + 'argumentCount' => '2', + ], + 'IMPRODUCT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexOperations::class, 'IMPRODUCT'], + 'argumentCount' => '1+', + ], + 'IMREAL' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\Complex::class, 'IMREAL'], + 'argumentCount' => '1', + ], + 'IMSEC' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSEC'], + 'argumentCount' => '1', + ], + 'IMSECH' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSECH'], + 'argumentCount' => '1', + ], + 'IMSIN' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSIN'], + 'argumentCount' => '1', + ], + 'IMSINH' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSINH'], + 'argumentCount' => '1', + ], + 'IMSQRT' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSQRT'], + 'argumentCount' => '1', + ], + 'IMSUB' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexOperations::class, 'IMSUB'], + 'argumentCount' => '2', + ], + 'IMSUM' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexOperations::class, 'IMSUM'], + 'argumentCount' => '1+', + ], + 'IMTAN' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMTAN'], + 'argumentCount' => '1', + ], + 'INDEX' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Matrix::class, 'index'], + 'argumentCount' => '2-4', + ], + 'INDIRECT' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Indirect::class, 'INDIRECT'], + 'argumentCount' => '1,2', + 'passCellReference' => true, + ], + 'INFO' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Info::class, 'getInfo'], + 'argumentCount' => '1', + 'passCellReference' => true, + ], + 'INT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\IntClass::class, 'evaluate'], + 'argumentCount' => '1', + ], + 'INTERCEPT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'INTERCEPT'], + 'argumentCount' => '2', + ], + 'INTRATE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Rates::class, 'interest'], + 'argumentCount' => '4,5', + ], + 'IPMT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'payment'], + 'argumentCount' => '4-6', + ], + 'IRR' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'rate'], + 'argumentCount' => '1,2', + ], + 'ISBLANK' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isBlank'], + 'argumentCount' => '1', + ], + 'ISERR' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\ErrorValue::class, 'isErr'], + 'argumentCount' => '1', + ], + 'ISERROR' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\ErrorValue::class, 'isError'], + 'argumentCount' => '1', + ], + 'ISEVEN' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isEven'], + 'argumentCount' => '1', + ], + 'ISFORMULA' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isFormula'], + 'argumentCount' => '1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'ISLOGICAL' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isLogical'], + 'argumentCount' => '1', + ], + 'ISNA' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\ErrorValue::class, 'isNa'], + 'argumentCount' => '1', + ], + 'ISNONTEXT' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isNonText'], + 'argumentCount' => '1', + ], + 'ISNUMBER' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isNumber'], + 'argumentCount' => '1', + ], + 'ISO.CEILING' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1,2', + ], + 'ISODD' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isOdd'], + 'argumentCount' => '1', + ], + 'ISOMITTED' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'ISOWEEKNUM' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Week::class, 'isoWeekNumber'], + 'argumentCount' => '1', + ], + 'ISPMT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'schedulePayment'], + 'argumentCount' => '4', + ], + 'ISREF' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isRef'], + 'argumentCount' => '1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'ISTEXT' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'isText'], + 'argumentCount' => '1', + ], + 'ISTHAIDIGIT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'JIS' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1', + ], + 'KURT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Deviations::class, 'kurtosis'], + 'argumentCount' => '1+', + ], + 'LAMBDA' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'LARGE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Size::class, 'large'], + 'argumentCount' => '2', + ], + 'LCM' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Lcm::class, 'evaluate'], + 'argumentCount' => '1+', + ], + 'LEFT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'left'], + 'argumentCount' => '1,2', + ], + 'LEFTB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'left'], + 'argumentCount' => '1,2', + ], + 'LEN' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'length'], + 'argumentCount' => '1', + ], + 'LENB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'length'], + 'argumentCount' => '1', + ], + 'LET' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'LINEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'LINEST'], + 'argumentCount' => '1-4', + ], + 'LN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Logarithms::class, 'natural'], + 'argumentCount' => '1', + ], + 'LOG' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Logarithms::class, 'withBase'], + 'argumentCount' => '1,2', + ], + 'LOG10' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Logarithms::class, 'base10'], + 'argumentCount' => '1', + ], + 'LOGEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'LOGEST'], + 'argumentCount' => '1-4', + ], + 'LOGINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'], + 'argumentCount' => '3', + ], + 'LOGNORMDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'cumulative'], + 'argumentCount' => '3', + ], + 'LOGNORM.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'distribution'], + 'argumentCount' => '4', + ], + 'LOGNORM.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'], + 'argumentCount' => '3', + ], + 'LOOKUP' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Lookup::class, 'lookup'], + 'argumentCount' => '2,3', + ], + 'LOWER' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CaseConvert::class, 'lower'], + 'argumentCount' => '1', + ], + 'MAKEARRAY' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'MAP' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'MATCH' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ExcelMatch::class, 'MATCH'], + 'argumentCount' => '2,3', + ], + 'MAX' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Maximum::class, 'max'], + 'argumentCount' => '1+', + ], + 'MAXA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Maximum::class, 'maxA'], + 'argumentCount' => '1+', + ], + 'MAXIFS' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Conditional::class, 'MAXIFS'], + 'argumentCount' => '3+', + ], + 'MDETERM' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\MatrixFunctions::class, 'determinant'], + 'argumentCount' => '1', + ], + 'MDURATION' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '5,6', + ], + 'MEDIAN' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages::class, 'median'], + 'argumentCount' => '1+', + ], + 'MEDIANIF' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2+', + ], + 'MID' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'mid'], + 'argumentCount' => '3', + ], + 'MIDB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'mid'], + 'argumentCount' => '3', + ], + 'MIN' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Minimum::class, 'min'], + 'argumentCount' => '1+', + ], + 'MINA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Minimum::class, 'minA'], + 'argumentCount' => '1+', + ], + 'MINIFS' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Conditional::class, 'MINIFS'], + 'argumentCount' => '3+', + ], + 'MINUTE' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\TimeParts::class, 'minute'], + 'argumentCount' => '1', + ], + 'MINVERSE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\MatrixFunctions::class, 'inverse'], + 'argumentCount' => '1', + ], + 'MIRR' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'modifiedRate'], + 'argumentCount' => '3', + ], + 'MMULT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\MatrixFunctions::class, 'multiply'], + 'argumentCount' => '2', + ], + 'MOD' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Operations::class, 'mod'], + 'argumentCount' => '2', + ], + 'MODE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages::class, 'mode'], + 'argumentCount' => '1+', + ], + 'MODE.MULT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1+', + ], + 'MODE.SNGL' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages::class, 'mode'], + 'argumentCount' => '1+', + ], + 'MONTH' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\DateParts::class, 'month'], + 'argumentCount' => '1', + ], + 'MROUND' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Round::class, 'multiple'], + 'argumentCount' => '2', + ], + 'MULTINOMIAL' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Factorial::class, 'multinomial'], + 'argumentCount' => '1+', + ], + 'MUNIT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\MatrixFunctions::class, 'identity'], + 'argumentCount' => '1', + ], + 'N' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'asNumber'], + 'argumentCount' => '1', + ], + 'NA' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\ExcelError::class, 'NA'], + 'argumentCount' => '0', + ], + 'NEGBINOMDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Binomial::class, 'negative'], + 'argumentCount' => '3', + ], + 'NEGBINOM.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '4', + ], + 'NETWORKDAYS' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\NetworkDays::class, 'count'], + 'argumentCount' => '2-3', + ], + 'NETWORKDAYS.INTL' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-4', + ], + 'NOMINAL' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\InterestRate::class, 'nominal'], + 'argumentCount' => '2', + ], + 'NORMDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'], + 'argumentCount' => '4', + ], + 'NORM.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'], + 'argumentCount' => '4', + ], + 'NORMINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'], + 'argumentCount' => '3', + ], + 'NORM.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'], + 'argumentCount' => '3', + ], + 'NORMSDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'cumulative'], + 'argumentCount' => '1', + ], + 'NORM.S.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'distribution'], + 'argumentCount' => '1,2', + ], + 'NORMSINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'], + 'argumentCount' => '1', + ], + 'NORM.S.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'], + 'argumentCount' => '1', + ], + 'NOT' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Operations::class, 'NOT'], + 'argumentCount' => '1', + ], + 'NOW' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Current::class, 'now'], + 'argumentCount' => '0', + ], + 'NPER' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'periods'], + 'argumentCount' => '3-5', + ], + 'NPV' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'presentValue'], + 'argumentCount' => '2+', + ], + 'NUMBERSTRING' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'NUMBERVALUE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Format::class, 'NUMBERVALUE'], + 'argumentCount' => '1+', + ], + 'OCT2BIN' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertOctal::class, 'toBinary'], + 'argumentCount' => '1,2', + ], + 'OCT2DEC' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertOctal::class, 'toDecimal'], + 'argumentCount' => '1', + ], + 'OCT2HEX' => [ + 'category' => Category::CATEGORY_ENGINEERING, + 'functionCall' => [Engineering\ConvertOctal::class, 'toHex'], + 'argumentCount' => '1,2', + ], + 'ODD' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Round::class, 'odd'], + 'argumentCount' => '1', + ], + 'ODDFPRICE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '8,9', + ], + 'ODDFYIELD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '8,9', + ], + 'ODDLPRICE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '7,8', + ], + 'ODDLYIELD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '7,8', + ], + 'OFFSET' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Offset::class, 'OFFSET'], + 'argumentCount' => '3-5', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'OR' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Operations::class, 'logicalOr'], + 'argumentCount' => '1+', + ], + 'PDURATION' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Single::class, 'periods'], + 'argumentCount' => '3', + ], + 'PEARSON' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'CORREL'], + 'argumentCount' => '2', + ], + 'PERCENTILE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'], + 'argumentCount' => '2', + ], + 'PERCENTILE.EXC' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'PERCENTILE.INC' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'], + 'argumentCount' => '2', + ], + 'PERCENTRANK' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'], + 'argumentCount' => '2,3', + ], + 'PERCENTRANK.EXC' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2,3', + ], + 'PERCENTRANK.INC' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'], + 'argumentCount' => '2,3', + ], + 'PERMUT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Permutations::class, 'PERMUT'], + 'argumentCount' => '2', + ], + 'PERMUTATIONA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Permutations::class, 'PERMUTATIONA'], + 'argumentCount' => '2', + ], + 'PHONETIC' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1', + ], + 'PHI' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1', + ], + 'PI' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => 'pi', + 'argumentCount' => '0', + ], + 'PMT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'annuity'], + 'argumentCount' => '3-5', + ], + 'POISSON' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'], + 'argumentCount' => '3', + ], + 'POISSON.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'], + 'argumentCount' => '3', + ], + 'POWER' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Operations::class, 'power'], + 'argumentCount' => '2', + ], + 'PPMT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'interestPayment'], + 'argumentCount' => '4-6', + ], + 'PRICE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Price::class, 'price'], + 'argumentCount' => '6,7', + ], + 'PRICEDISC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Price::class, 'priceDiscounted'], + 'argumentCount' => '4,5', + ], + 'PRICEMAT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Price::class, 'priceAtMaturity'], + 'argumentCount' => '5,6', + ], + 'PROB' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3,4', + ], + 'PRODUCT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Operations::class, 'product'], + 'argumentCount' => '1+', + ], + 'PROPER' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CaseConvert::class, 'proper'], + 'argumentCount' => '1', + ], + 'PV' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'presentValue'], + 'argumentCount' => '3-5', + ], + 'QUARTILE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'], + 'argumentCount' => '2', + ], + 'QUARTILE.EXC' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'QUARTILE.INC' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'], + 'argumentCount' => '2', + ], + 'QUOTIENT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Operations::class, 'quotient'], + 'argumentCount' => '2', + ], + 'RADIANS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Angle::class, 'toRadians'], + 'argumentCount' => '1', + ], + 'RAND' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Random::class, 'rand'], + 'argumentCount' => '0', + ], + 'RANDARRAY' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Random::class, 'randArray'], + 'argumentCount' => '0-5', + ], + 'RANDBETWEEN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Random::class, 'randBetween'], + 'argumentCount' => '2', + ], + 'RANK' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'RANK'], + 'argumentCount' => '2,3', + ], + 'RANK.AVG' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2,3', + ], + 'RANK.EQ' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Percentiles::class, 'RANK'], + 'argumentCount' => '2,3', + ], + 'RATE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'rate'], + 'argumentCount' => '3-6', + ], + 'RECEIVED' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Price::class, 'received'], + 'argumentCount' => '4-5', + ], + 'REDUCE' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'REPLACE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Replace::class, 'replace'], + 'argumentCount' => '4', + ], + 'REPLACEB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Replace::class, 'replace'], + 'argumentCount' => '4', + ], + 'REPT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Concatenate::class, 'builtinREPT'], + 'argumentCount' => '2', + ], + 'RIGHT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'right'], + 'argumentCount' => '1,2', + ], + 'RIGHTB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'right'], + 'argumentCount' => '1,2', + ], + 'ROMAN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Roman::class, 'evaluate'], + 'argumentCount' => '1,2', + ], + 'ROUND' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Round::class, 'round'], + 'argumentCount' => '2', + ], + 'ROUNDBAHTDOWN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'ROUNDBAHTUP' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'ROUNDDOWN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Round::class, 'down'], + 'argumentCount' => '2', + ], + 'ROUNDUP' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Round::class, 'up'], + 'argumentCount' => '2', + ], + 'ROW' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\RowColumnInformation::class, 'ROW'], + 'argumentCount' => '-1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'ROWS' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\RowColumnInformation::class, 'ROWS'], + 'argumentCount' => '1', + ], + 'RRI' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Single::class, 'interestRate'], + 'argumentCount' => '3', + ], + 'RSQ' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'RSQ'], + 'argumentCount' => '2', + ], + 'RTD' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1+', + ], + 'SEARCH' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Search::class, 'insensitive'], + 'argumentCount' => '2,3', + ], + 'SCAN' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '*', + ], + 'SEARCHB' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Search::class, 'insensitive'], + 'argumentCount' => '2,3', + ], + 'SEC' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Secant::class, 'sec'], + 'argumentCount' => '1', + ], + 'SECH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Secant::class, 'sech'], + 'argumentCount' => '1', + ], + 'SECOND' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\TimeParts::class, 'second'], + 'argumentCount' => '1', + ], + 'SEQUENCE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\MatrixFunctions::class, 'sequence'], + 'argumentCount' => '1-4', + ], + 'SERIESSUM' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\SeriesSum::class, 'evaluate'], + 'argumentCount' => '4', + ], + 'SHEET' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '0,1', + ], + 'SHEETS' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '0,1', + ], + 'SIGN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Sign::class, 'evaluate'], + 'argumentCount' => '1', + ], + 'SIN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Sine::class, 'sin'], + 'argumentCount' => '1', + ], + 'SINGLE' => [ + 'category' => Category::CATEGORY_MICROSOFT_INTERNAL, + 'functionCall' => [Internal\ExcelArrayPseudoFunctions::class, 'single'], + 'argumentCount' => '1', + 'passCellReference' => true, + 'passByReference' => [true], + ], + 'SINH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Sine::class, 'sinh'], + 'argumentCount' => '1', + ], + 'SKEW' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Deviations::class, 'skew'], + 'argumentCount' => '1+', + ], + 'SKEW.P' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1+', + ], + 'SLN' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Depreciation::class, 'SLN'], + 'argumentCount' => '3', + ], + 'SLOPE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'SLOPE'], + 'argumentCount' => '2', + ], + 'SMALL' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Size::class, 'small'], + 'argumentCount' => '2', + ], + 'SORT' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Sort::class, 'sort'], + 'argumentCount' => '1-4', + ], + 'SORTBY' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Sort::class, 'sortBy'], + 'argumentCount' => '2+', + ], + 'SQRT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Sqrt::class, 'sqrt'], + 'argumentCount' => '1', + ], + 'SQRTPI' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Sqrt::class, 'pi'], + 'argumentCount' => '1', + ], + 'STANDARDIZE' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Standardize::class, 'execute'], + 'argumentCount' => '3', + ], + 'STDEV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'], + 'argumentCount' => '1+', + ], + 'STDEV.S' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'], + 'argumentCount' => '1+', + ], + 'STDEV.P' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'], + 'argumentCount' => '1+', + ], + 'STDEVA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVA'], + 'argumentCount' => '1+', + ], + 'STDEVP' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'], + 'argumentCount' => '1+', + ], + 'STDEVPA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVPA'], + 'argumentCount' => '1+', + ], + 'STEYX' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'STEYX'], + 'argumentCount' => '2', + ], + 'SUBSTITUTE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Replace::class, 'substitute'], + 'argumentCount' => '3,4', + ], + 'SUBTOTAL' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Subtotal::class, 'evaluate'], + 'argumentCount' => '2+', + 'passCellReference' => true, + ], + 'SUM' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Sum::class, 'sumErroringStrings'], + 'argumentCount' => '1+', + ], + 'SUMIF' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Statistical\Conditional::class, 'SUMIF'], + 'argumentCount' => '2,3', + ], + 'SUMIFS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Statistical\Conditional::class, 'SUMIFS'], + 'argumentCount' => '3+', + ], + 'SUMPRODUCT' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Sum::class, 'product'], + 'argumentCount' => '1+', + ], + 'SUMSQ' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\SumSquares::class, 'sumSquare'], + 'argumentCount' => '1+', + ], + 'SUMX2MY2' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredMinusYSquared'], + 'argumentCount' => '2', + ], + 'SUMX2PY2' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredPlusYSquared'], + 'argumentCount' => '2', + ], + 'SUMXMY2' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\SumSquares::class, 'sumXMinusYSquared'], + 'argumentCount' => '2', + ], + 'SWITCH' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Conditional::class, 'statementSwitch'], + 'argumentCount' => '3+', + ], + 'SYD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Depreciation::class, 'SYD'], + 'argumentCount' => '4', + ], + 'T' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'test'], + 'argumentCount' => '1', + ], + 'TAKE' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'take'], + 'argumentCount' => '2-3', + ], + 'TAN' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Tangent::class, 'tan'], + 'argumentCount' => '1', + ], + 'TANH' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trig\Tangent::class, 'tanh'], + 'argumentCount' => '1', + ], + 'TBILLEQ' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\TreasuryBill::class, 'bondEquivalentYield'], + 'argumentCount' => '3', + ], + 'TBILLPRICE' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\TreasuryBill::class, 'price'], + 'argumentCount' => '3', + ], + 'TBILLYIELD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\TreasuryBill::class, 'yield'], + 'argumentCount' => '3', + ], + 'TDIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StudentT::class, 'distribution'], + 'argumentCount' => '3', + ], + 'T.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3', + ], + 'T.DIST.2T' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'T.DIST.RT' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'TEXT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Format::class, 'TEXTFORMAT'], + 'argumentCount' => '2', + ], + 'TEXTAFTER' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'after'], + 'argumentCount' => '2-6', + ], + 'TEXTBEFORE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'before'], + 'argumentCount' => '2-6', + ], + 'TEXTJOIN' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Concatenate::class, 'TEXTJOIN'], + 'argumentCount' => '3+', + ], + 'TEXTSPLIT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'split'], + 'argumentCount' => '2-6', + ], + 'THAIDAYOFWEEK' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAIDIGIT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAIMONTHOFYEAR' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAINUMSOUND' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAINUMSTRING' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAISTRINGLENGTH' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'THAIYEAR' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '?', + ], + 'TIME' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Time::class, 'fromHMS'], + 'argumentCount' => '3', + ], + 'TIMEVALUE' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\TimeValue::class, 'fromString'], + 'argumentCount' => '1', + ], + 'TINV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'], + 'argumentCount' => '2', + ], + 'T.INV' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'], + 'argumentCount' => '2', + ], + 'T.INV.2T' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2', + ], + 'TODAY' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Current::class, 'today'], + 'argumentCount' => '0', + ], + 'TOCOL' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\TorowTocol::class, 'tocol'], + 'argumentCount' => '1-3', + ], + 'TOROW' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\TorowTocol::class, 'torow'], + 'argumentCount' => '1-3', + ], + 'TRANSPOSE' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Matrix::class, 'transpose'], + 'argumentCount' => '1', + ], + 'TREND' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Trends::class, 'TREND'], + 'argumentCount' => '1-4', + ], + 'TRIM' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Trim::class, 'spaces'], + 'argumentCount' => '1', + ], + 'TRIMMEAN' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Averages\Mean::class, 'trim'], + 'argumentCount' => '2', + ], + 'TRUE' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Boolean::class, 'TRUE'], + 'argumentCount' => '0', + ], + 'TRUNC' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [MathTrig\Trunc::class, 'evaluate'], + 'argumentCount' => '1,2', + ], + 'TTEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '4', + ], + 'T.TEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '4', + ], + 'TYPE' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Information\Value::class, 'type'], + 'argumentCount' => '1', + ], + 'UNICHAR' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CharacterConvert::class, 'character'], + 'argumentCount' => '1', + ], + 'UNICODE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CharacterConvert::class, 'code'], + 'argumentCount' => '1', + ], + 'UNIQUE' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Unique::class, 'unique'], + 'argumentCount' => '1+', + ], + 'UPPER' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\CaseConvert::class, 'upper'], + 'argumentCount' => '1', + ], + 'USDOLLAR' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Dollar::class, 'format'], + 'argumentCount' => '2', + ], + 'VALUE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Format::class, 'VALUE'], + 'argumentCount' => '1', + ], + 'VALUETOTEXT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Format::class, 'valueToText'], + 'argumentCount' => '1,2', + ], + 'VAR' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Variances::class, 'VAR'], + 'argumentCount' => '1+', + ], + 'VAR.P' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Variances::class, 'VARP'], + 'argumentCount' => '1+', + ], + 'VAR.S' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Variances::class, 'VAR'], + 'argumentCount' => '1+', + ], + 'VARA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Variances::class, 'VARA'], + 'argumentCount' => '1+', + ], + 'VARP' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Variances::class, 'VARP'], + 'argumentCount' => '1+', + ], + 'VARPA' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Variances::class, 'VARPA'], + 'argumentCount' => '1+', + ], + 'VDB' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '5-7', + ], + 'VLOOKUP' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\VLookup::class, 'lookup'], + 'argumentCount' => '3,4', + ], + 'VSTACK' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Vstack::class, 'vstack'], + 'argumentCount' => '1+', + ], + 'WEBSERVICE' => [ + 'category' => Category::CATEGORY_WEB, + 'functionCall' => [Web\Service::class, 'webService'], + 'argumentCount' => '1', + ], + 'WEEKDAY' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Week::class, 'day'], + 'argumentCount' => '1,2', + ], + 'WEEKNUM' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\Week::class, 'number'], + 'argumentCount' => '1,2', + ], + 'WEIBULL' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'], + 'argumentCount' => '4', + ], + 'WEIBULL.DIST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'], + 'argumentCount' => '4', + ], + 'WORKDAY' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\WorkDay::class, 'date'], + 'argumentCount' => '2-3', + ], + 'WORKDAY.INTL' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-4', + ], + 'WRAPCOLS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-3', + ], + 'WRAPROWS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-3', + ], + 'XIRR' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'rate'], + 'argumentCount' => '2,3', + ], + 'XLOOKUP' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3-6', + ], + 'XNPV' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'presentValue'], + 'argumentCount' => '3', + ], + 'XMATCH' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2,3', + ], + 'XOR' => [ + 'category' => Category::CATEGORY_LOGICAL, + 'functionCall' => [Logical\Operations::class, 'logicalXor'], + 'argumentCount' => '1+', + ], + 'YEAR' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\DateParts::class, 'year'], + 'argumentCount' => '1', + ], + 'YEARFRAC' => [ + 'category' => Category::CATEGORY_DATE_AND_TIME, + 'functionCall' => [DateTimeExcel\YearFrac::class, 'fraction'], + 'argumentCount' => '2,3', + ], + 'YIELD' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '6,7', + ], + 'YIELDDISC' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Yields::class, 'yieldDiscounted'], + 'argumentCount' => '4,5', + ], + 'YIELDMAT' => [ + 'category' => Category::CATEGORY_FINANCIAL, + 'functionCall' => [Financial\Securities\Yields::class, 'yieldAtMaturity'], + 'argumentCount' => '5,6', + ], + 'ZTEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'], + 'argumentCount' => '2-3', + ], + 'Z.TEST' => [ + 'category' => Category::CATEGORY_STATISTICAL, + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'], + 'argumentCount' => '2-3', + ], + ]; +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Functions.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Functions.php new file mode 100644 index 0000000..16f6a3d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Functions.php @@ -0,0 +1,363 @@ + 0); + } + + public static function isValue(mixed $idx): bool + { + $idx = StringHelper::convertToString($idx); + + return substr_count($idx, '.') === 0; + } + + public static function isCellValue(mixed $idx): bool + { + $idx = StringHelper::convertToString($idx); + + return substr_count($idx, '.') > 1; + } + + public static function ifCondition(mixed $condition): string + { + $condition = self::flattenSingleValue($condition); + + if ($condition === '' || $condition === null) { + return '=""'; + } + if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) { + $condition = self::operandSpecialHandling($condition); + if (is_bool($condition)) { + return '=' . ($condition ? 'TRUE' : 'FALSE'); + } + if (!is_numeric($condition)) { + if ($condition !== '""') { // Not an empty string + // Escape any quotes in the string value + $condition = (string) preg_replace('/"/ui', '""', $condition); + } + $condition = Calculation::wrapResult(strtoupper($condition)); + } + + return str_replace('""""', '""', '=' . StringHelper::convertToString($condition)); + } + $operator = $operand = ''; + if (1 === preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches)) { + [, $operator, $operand] = $matches; + } + + $operand = (string) self::operandSpecialHandling($operand); + if (is_numeric(trim($operand, '"'))) { + $operand = trim($operand, '"'); + } elseif (!is_numeric($operand) && $operand !== 'FALSE' && $operand !== 'TRUE') { + $operand = str_replace('"', '""', $operand); + $operand = Calculation::wrapResult(strtoupper($operand)); + $operand = StringHelper::convertToString($operand); + } + + return str_replace('""""', '""', $operator . $operand); + } + + private static function operandSpecialHandling(mixed $operand): bool|float|int|string + { + if (is_numeric($operand) || is_bool($operand)) { + return $operand; + } + $operand = StringHelper::convertToString($operand); + if (strtoupper($operand) === Calculation::getTRUE() || strtoupper($operand) === Calculation::getFALSE()) { + return strtoupper($operand); + } + + // Check for percentage + if (preg_match('/^\-?\d*\.?\d*\s?\%$/', $operand)) { + return ((float) rtrim($operand, '%')) / 100; + } + + // Check for dates + if (($dateValueOperand = Date::stringToExcel($operand)) !== false) { + return $dateValueOperand; + } + + return $operand; + } + + /** + * Convert a multi-dimensional array to a simple 1-dimensional array. + * + * @param mixed $array Array to be flattened + * + * @return array Flattened array + */ + public static function flattenArray(mixed $array): array + { + if (!is_array($array)) { + return (array) $array; + } + + $flattened = []; + $stack = array_values($array); + + while (!empty($stack)) { + $value = array_shift($stack); + + if (is_array($value)) { + array_unshift($stack, ...array_values($value)); + } else { + $flattened[] = $value; + } + } + + return $flattened; + } + + /** + * Convert a multi-dimensional array to a simple 1-dimensional array. + * Same as above but argument is specified in ... format. + * + * @param mixed $array Array to be flattened + * + * @return array Flattened array + */ + public static function flattenArray2(mixed ...$array): array + { + $flattened = []; + $stack = array_values($array); + + while (!empty($stack)) { + $value = array_shift($stack); + + if (is_array($value)) { + array_unshift($stack, ...array_values($value)); + } else { + $flattened[] = $value; + } + } + + return $flattened; + } + + public static function scalar(mixed $value): mixed + { + if (!is_array($value)) { + return $value; + } + + do { + $value = array_pop($value); + } while (is_array($value)); + + return $value; + } + + /** + * Convert a multi-dimensional array to a simple 1-dimensional array, but retain an element of indexing. + * + * @param array|mixed $array Array to be flattened + * + * @return array Flattened array + */ + public static function flattenArrayIndexed($array): array + { + if (!is_array($array)) { + return (array) $array; + } + + $arrayValues = []; + foreach ($array as $k1 => $value) { + if (is_array($value)) { + foreach ($value as $k2 => $val) { + if (is_array($val)) { + foreach ($val as $k3 => $v) { + $arrayValues[$k1 . '.' . $k2 . '.' . $k3] = $v; + } + } else { + $arrayValues[$k1 . '.' . $k2] = $val; + } + } + } else { + $arrayValues[$k1] = $value; + } + } + + return $arrayValues; + } + + /** + * Convert an array to a single scalar value by extracting the first element. + * + * @param mixed $value Array or scalar value + */ + public static function flattenSingleValue(mixed $value): mixed + { + while (is_array($value)) { + $value = array_shift($value); + } + + return $value; + } + + public static function expandDefinedName(string $coordinate, Cell $cell): string + { + $worksheet = $cell->getWorksheet(); + $spreadsheet = $worksheet->getParentOrThrow(); + // Uppercase coordinate + $pCoordinatex = strtoupper($coordinate); + // Eliminate leading equal sign + $pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex); + $defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet); + if ($defined !== null) { + $worksheet2 = $defined->getWorkSheet(); + if (!$defined->isFormula() && $worksheet2 !== null) { + $coordinate = "'" . $worksheet2->getTitle() . "'!" + . (string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue())); + } + } + + return $coordinate; + } + + public static function trimTrailingRange(string $coordinate): string + { + return (string) preg_replace('/:[\w\$]+$/', '', $coordinate); + } + + public static function trimSheetFromCellReference(string $coordinate): string + { + if (str_contains($coordinate, '!')) { + $coordinate = substr($coordinate, strrpos($coordinate, '!') + 1); + } + + return $coordinate; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php new file mode 100644 index 0000000..00a5d65 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -0,0 +1,72 @@ +|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isErr(mixed $value = ''): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return self::isError($value) && (!self::isNa(($value))); + } + + /** + * IS_ERROR. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isError(mixed $value = '', bool $tryNotImplemented = false): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if (!is_string($value)) { + return false; + } + if ($tryNotImplemented && $value === Functions::NOT_YET_IMPLEMENTED) { + return true; + } + + return in_array($value, ExcelError::ERROR_CODES, true); + } + + /** + * IS_NA. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isNa(mixed $value = ''): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return $value === ExcelError::NA(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/ExcelError.php new file mode 100644 index 0000000..94a4a7c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -0,0 +1,167 @@ + + */ + public const ERROR_CODES = [ + 'null' => '#NULL!', // 1 + 'divisionbyzero' => '#DIV/0!', // 2 + 'value' => '#VALUE!', // 3 + 'reference' => '#REF!', // 4 + 'name' => '#NAME?', // 5 + 'num' => '#NUM!', // 6 + 'na' => '#N/A', // 7 + 'gettingdata' => '#GETTING_DATA', // 8 + 'spill' => '#SPILL!', // 9 + 'connect' => '#CONNECT!', //10 + 'blocked' => '#BLOCKED!', //11 + 'unknown' => '#UNKNOWN!', //12 + 'field' => '#FIELD!', //13 + 'calculation' => '#CALC!', //14 + ]; + + public static function throwError(mixed $value): string + { + return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value']; + } + + /** + * ERROR_TYPE. + * + * @param mixed $value Value to check + * + * @return array|int|string + */ + public static function type(mixed $value = ''): array|int|string + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + $i = 1; + foreach (self::ERROR_CODES as $errorCode) { + if ($value === $errorCode) { + return $i; + } + ++$i; + } + + return self::NA(); + } + + /** + * NULL. + * + * Returns the error value #NULL! + * + * @return string #NULL! + */ + public static function null(): string + { + return self::ERROR_CODES['null']; + } + + /** + * NaN. + * + * Returns the error value #NUM! + * + * @return string #NUM! + */ + public static function NAN(): string + { + return self::ERROR_CODES['num']; + } + + /** + * REF. + * + * Returns the error value #REF! + * + * @return string #REF! + */ + public static function REF(): string + { + return self::ERROR_CODES['reference']; + } + + /** + * NA. + * + * Excel Function: + * =NA() + * + * Returns the error value #N/A + * #N/A is the error value that means "no value is available." + * + * @return string #N/A! + */ + public static function NA(): string + { + return self::ERROR_CODES['na']; + } + + /** + * VALUE. + * + * Returns the error value #VALUE! + * + * @return string #VALUE! + */ + public static function VALUE(): string + { + return self::ERROR_CODES['value']; + } + + /** + * NAME. + * + * Returns the error value #NAME? + * + * @return string #NAME? + */ + public static function NAME(): string + { + return self::ERROR_CODES['name']; + } + + /** + * DIV0. + * + * @return string #DIV/0! + */ + public static function DIV0(): string + { + return self::ERROR_CODES['divisionbyzero']; + } + + /** + * CALC. + * + * @return string #CALC! + */ + public static function CALC(): string + { + return self::ERROR_CODES['calculation']; + } + + /** + * SPILL. + * + * @return string #SPILL! + */ + public static function SPILL(): string + { + return self::ERROR_CODES['spill']; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/Info.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/Info.php new file mode 100644 index 0000000..61138fe --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/Info.php @@ -0,0 +1,44 @@ + '/', + 'numfile' => $cell?->getWorksheetOrNull()?->getParent()?->getSheetCount() ?? 1, + 'origin' => '$A:$A$1', + 'osversion' => 'PHP ' . PHP_VERSION, + 'recalc' => 'Automatic', + 'release' => PHP_VERSION, + 'system' => 'PHP', + 'memavail', 'memused', 'totmem' => ExcelError::NA(), + default => ExcelError::VALUE(), + }; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/Value.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/Value.php new file mode 100644 index 0000000..2f949cd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -0,0 +1,326 @@ +|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isBlank(mixed $value = null): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return $value === null; + } + + /** + * IS_REF. + * + * @param mixed $value Value to check + */ + public static function isRef(mixed $value, ?Cell $cell = null): bool + { + if ($cell === null) { + return false; + } + + $value = StringHelper::convertToString($value); + $cellValue = Functions::trimTrailingRange($value); + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/ui', $cellValue) === 1) { + [$worksheet, $cellValue] = Worksheet::extractSheetTitle($cellValue, true, true); + if (!empty($worksheet) && $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheet) === null) { + return false; + } + [$column, $row] = Coordinate::indexesFromString($cellValue ?? ''); + if ($column > 16384 || $row > 1048576) { + return false; + } + + return true; + } + + $namedRange = $cell->getWorksheet()->getParentOrThrow()->getNamedRange($value); + + return $namedRange instanceof NamedRange; + } + + /** + * IS_EVEN. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isEven(mixed $value = null): array|string|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if ($value === null) { + return ExcelError::NAME(); + } + if (!is_numeric($value)) { + return ExcelError::VALUE(); + } + + return ((int) fmod($value + 0, 2)) === 0; + } + + /** + * IS_ODD. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isOdd(mixed $value = null): array|string|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if ($value === null) { + return ExcelError::NAME(); + } + if (!is_numeric($value)) { + return ExcelError::VALUE(); + } + + return ((int) fmod($value + 0, 2)) !== 0; + } + + /** + * IS_NUMBER. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isNumber(mixed $value = null): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if (is_string($value)) { + return false; + } + + return is_numeric($value); + } + + /** + * IS_LOGICAL. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isLogical(mixed $value = null): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return is_bool($value); + } + + /** + * IS_TEXT. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isText(mixed $value = null): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return is_string($value) && !ErrorValue::isError($value); + } + + /** + * IS_NONTEXT. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isNonText(mixed $value = null): array|bool + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return !self::isText($value); + } + + /** + * ISFORMULA. + * + * @param mixed $cellReference The cell to check + * @param ?Cell $cell The current cell (containing this formula) + * + * @return array|bool|string + */ + public static function isFormula(mixed $cellReference = '', ?Cell $cell = null): array|bool|string + { + if ($cell === null) { + return ExcelError::REF(); + } + $cellReference = StringHelper::convertToString($cellReference); + + $fullCellReference = Functions::expandDefinedName($cellReference, $cell); + + if (str_contains($cellReference, '!')) { + $cellReference = Functions::trimSheetFromCellReference($cellReference); + $cellReferences = Coordinate::extractAllCellReferencesInRange($cellReference); + if (count($cellReferences) > 1) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $cellReferences, $cell); + } + } + + $fullCellReference = Functions::trimTrailingRange($fullCellReference); + + $worksheetName = ''; + if (1 == preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $fullCellReference, $matches)) { + $fullCellReference = $matches[6] . $matches[7]; + $worksheetName = str_replace("''", "'", trim($matches[2], "'")); + } + + $worksheet = (!empty($worksheetName)) + ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName) + : $cell->getWorksheet(); + if ($worksheet === null) { + return ExcelError::REF(); + } + + try { + return $worksheet->getCell($fullCellReference)->isFormula(); + } catch (SpreadsheetException) { + return true; + } + } + + /** + * N. + * + * Returns a value converted to a number + * + * @param null|mixed $value The value you want converted + * + * @return number|string N converts values listed in the following table + * If value is or refers to N returns + * A number That number value + * A date The Excel serialized number of that date + * TRUE 1 + * FALSE 0 + * An error value The error value + * Anything else 0 + */ + public static function asNumber($value = null) + { + while (is_array($value)) { + $value = array_shift($value); + } + if (is_float($value) || is_int($value)) { + return $value; + } + if (is_bool($value)) { + return (int) $value; + } + if (is_string($value) && str_starts_with($value, '#')) { + return $value; + } + + return 0; + } + + /** + * TYPE. + * + * Returns a number that identifies the type of a value + * + * @param null|mixed $value The value you want tested + * + * @return int N converts values listed in the following table + * If value is or refers to N returns + * A number 1 + * Text 2 + * Logical Value 4 + * An error value 16 + * Array or Matrix 64 + */ + public static function type($value = null): int + { + $value = Functions::flattenArrayIndexed($value); + if (count($value) > 1) { + end($value); + $a = key($value); + // Range of cells is an error + if (Functions::isCellValue($a)) { + return 16; + // Test for Matrix + } elseif (Functions::isMatrixValue($a)) { + return 64; + } + } elseif (empty($value)) { + // Empty Cell + return 1; + } + + $value = Functions::flattenSingleValue($value); + if (($value === null) || (is_float($value)) || (is_int($value))) { + return 1; + } elseif (is_bool($value)) { + return 4; + } elseif (is_array($value)) { + return 64; + } elseif (is_string($value)) { + // Errors + if (($value !== '') && ($value[0] == '#')) { + return 16; + } + + return 2; + } + + return 0; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php new file mode 100644 index 0000000..8cce0b2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php @@ -0,0 +1,104 @@ +getWorksheet(); + + [$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true, true); + if (preg_match('/^([$]?[a-z]{1,3})([$]?([0-9]{1,7})):([$]?[a-z]{1,3})([$]?([0-9]{1,7}))$/i', "$referenceCellCoordinate", $matches) === 1) { + $ourRow = $cell->getRow(); + $firstRow = (int) $matches[3]; + $lastRow = (int) $matches[6]; + if ($ourRow < $firstRow || $ourRow > $lastRow || $matches[1] !== $matches[4]) { + return ExcelError::VALUE(); + } + $referenceCellCoordinate = $matches[1] . $ourRow; + } + $referenceCell = ($referenceWorksheetName === '') + ? $worksheet->getCell((string) $referenceCellCoordinate) + : $worksheet->getParentOrThrow() + ->getSheetByNameOrThrow((string) $referenceWorksheetName) + ->getCell((string) $referenceCellCoordinate); + + $result = $referenceCell->getCalculatedValue(); + while (is_array($result)) { + $result = array_shift($result); + } + + return $result; + } + + /** @return array|string */ + public static function anchorArray(string $cellReference, Cell $cell): array|string + { + //$coordinate = $cell->getCoordinate(); + $worksheet = $cell->getWorksheet(); + + [$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true, true); + $referenceCell = ($referenceWorksheetName === '') + ? $worksheet->getCell((string) $referenceCellCoordinate) + : $worksheet->getParentOrThrow() + ->getSheetByNameOrThrow((string) $referenceWorksheetName) + ->getCell((string) $referenceCellCoordinate); + + // We should always use the sizing for the array formula range from the referenced cell formula + //$referenceRange = null; + /*if ($referenceCell->isFormula() && $referenceCell->isArrayFormula()) { + $referenceRange = $referenceCell->arrayFormulaRange(); + }*/ + + $calcEngine = Calculation::getInstance($worksheet->getParent()); + $result = $calcEngine->calculateCellValue($referenceCell, false); + if (!is_array($result)) { + $result = ExcelError::REF(); + } + + // Ensure that our array result dimensions match the specified array formula range dimensions, + // from the referenced cell, expanding or shrinking it as necessary. + /*$result = Functions::resizeMatrix( + $result, + ...Coordinate::rangeDimension($referenceRange ?? $coordinate) + );*/ + + // Set the result for our target cell (with spillage) + // But if we do write it, we get problems with #SPILL! Errors if the spreadsheet is saved + // TODO How are we going to identify and handle a #SPILL! or a #CALC! error? +// IOFactory::setLoading(true); +// $worksheet->fromArray( +// $result, +// null, +// $coordinate, +// true +// ); +// IOFactory::setLoading(true); + + // Calculate the array formula range that we should set for our target, based on our target cell coordinate +// [$col, $row] = Coordinate::indexesFromString($coordinate); +// $row += count($result) - 1; +// $col = Coordinate::stringFromColumnIndex($col + count($result[0]) - 1); +// $arrayFormulaRange = "{$coordinate}:{$col}{$row}"; +// $formulaAttributes = ['t' => 'array', 'ref' => $arrayFormulaRange]; + + // Using fromArray() would reset the value for this cell with the calculation result + // as well as updating the spillage cells, + // so we need to restore this cell to its formula value, attributes, and datatype +// $cell = $worksheet->getCell($coordinate); +// $cell->setValueExplicit($value, DataType::TYPE_FORMULA, true, $arrayFormulaRange); +// $cell->setFormulaAttributes($formulaAttributes); + +// $cell->updateInCollection(); + + return $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php new file mode 100644 index 0000000..f289c3f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php @@ -0,0 +1,16 @@ + 0) { + $targetValue = Functions::flattenSingleValue($arguments[0]); + $argc = count($arguments) - 1; + $switchCount = floor($argc / 2); + $hasDefaultClause = $argc % 2 !== 0; + $defaultClause = $argc % 2 === 0 ? null : $arguments[$argc]; + + $switchSatisfied = false; + if ($switchCount > 0) { + for ($index = 0; $index < $switchCount; ++$index) { + if ($targetValue == Functions::flattenSingleValue($arguments[$index * 2 + 1])) { + $result = $arguments[$index * 2 + 2]; + $switchSatisfied = true; + + break; + } + } + } + + if ($switchSatisfied !== true) { + $result = $hasDefaultClause ? $defaultClause : ExcelError::NA(); + } + } + + return $result; + } + + /** + * IFERROR. + * + * Excel Function: + * =IFERROR(testValue,errorpart) + * + * @param mixed $testValue Value to check, is also the value returned when no error + * Or can be an array of values + * @param mixed $errorpart Value to return when testValue is an error condition + * Note that this can be an array value to be returned + * + * @return mixed The value of errorpart or testValue determined by error condition + * If an array of values is passed as the $testValue argument, then the returned result will also be + * an array with the same dimensions + */ + public static function IFERROR(mixed $testValue = '', mixed $errorpart = ''): mixed + { + if (is_array($testValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $testValue, $errorpart); + } + + $errorpart = $errorpart ?? ''; + $testValue = $testValue ?? 0; // this is how Excel handles empty cell + + return self::statementIf(ErrorValue::isError($testValue), $errorpart, $testValue); + } + + /** + * IFNA. + * + * Excel Function: + * =IFNA(testValue,napart) + * + * @param mixed $testValue Value to check, is also the value returned when not an NA + * Or can be an array of values + * @param mixed $napart Value to return when testValue is an NA condition + * Note that this can be an array value to be returned + * + * @return mixed The value of errorpart or testValue determined by error condition + * If an array of values is passed as the $testValue argument, then the returned result will also be + * an array with the same dimensions + */ + public static function IFNA(mixed $testValue = '', mixed $napart = ''): mixed + { + if (is_array($testValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $testValue, $napart); + } + + $napart = $napart ?? ''; + $testValue = $testValue ?? 0; // this is how Excel handles empty cell + + return self::statementIf(ErrorValue::isNa($testValue), $napart, $testValue); + } + + /** + * IFS. + * + * Excel Function: + * =IFS(testValue1;returnIfTrue1;testValue2;returnIfTrue2;...;testValue_n;returnIfTrue_n) + * + * testValue1 ... testValue_n + * Conditions to Evaluate + * returnIfTrue1 ... returnIfTrue_n + * Value returned if corresponding testValue (nth) was true + * + * @param mixed ...$arguments Statement arguments + * Note that this can be an array value to be returned + * + * @return mixed|string The value of returnIfTrue_n, if testValue_n was true. #N/A if none of testValues was true + */ + public static function IFS(mixed ...$arguments) + { + $argumentCount = count($arguments); + + if ($argumentCount % 2 != 0) { + return ExcelError::NA(); + } + // We use instance of Exception as a falseValue in order to prevent string collision with value in cell + $falseValueException = new Exception(); + for ($i = 0; $i < $argumentCount; $i += 2) { + $testValue = ($arguments[$i] === null) ? '' : Functions::flattenSingleValue($arguments[$i]); + $returnIfTrue = ($arguments[$i + 1] === null) ? '' : $arguments[$i + 1]; + $result = self::statementIf($testValue, $returnIfTrue, $falseValueException); + + if ($result !== $falseValueException) { + return $result; + } + } + + return ExcelError::NA(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Logical/Operations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Logical/Operations.php new file mode 100644 index 0000000..f0a5476 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Logical/Operations.php @@ -0,0 +1,167 @@ + $trueValueCount === $count); + } + + /** + * LOGICAL_OR. + * + * Returns boolean TRUE if any argument is TRUE; returns FALSE if all arguments are FALSE. + * + * Excel Function: + * =OR(logical1[,logical2[, ...]]) + * + * The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays + * or references that contain logical values. + * + * Boolean arguments are treated as True or False as appropriate + * Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False + * If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string + * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value + * + * @param mixed $args Data values + * + * @return bool|string the logical OR of the arguments + */ + public static function logicalOr(mixed ...$args) + { + return self::countTrueValues($args, fn (int $trueValueCount): bool => $trueValueCount > 0); + } + + /** + * LOGICAL_XOR. + * + * Returns the Exclusive Or logical operation for one or more supplied conditions. + * i.e. the Xor function returns TRUE if an odd number of the supplied conditions evaluate to TRUE, + * and FALSE otherwise. + * + * Excel Function: + * =XOR(logical1[,logical2[, ...]]) + * + * The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays + * or references that contain logical values. + * + * Boolean arguments are treated as True or False as appropriate + * Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False + * If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string + * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value + * + * @param mixed $args Data values + * + * @return bool|string the logical XOR of the arguments + */ + public static function logicalXor(mixed ...$args) + { + return self::countTrueValues($args, fn (int $trueValueCount): bool => $trueValueCount % 2 === 1); + } + + /** + * NOT. + * + * Returns the boolean inverse of the argument. + * + * Excel Function: + * =NOT(logical) + * + * The argument must evaluate to a logical value such as TRUE or FALSE + * + * Boolean arguments are treated as True or False as appropriate + * Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False + * If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string + * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value + * + * @param mixed $logical A value or expression that can be evaluated to TRUE or FALSE + * Or can be an array of values + * + * @return array|bool|string the boolean inverse of the argument + * If an array of values is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function NOT(mixed $logical = false): array|bool|string + { + if (is_array($logical)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $logical); + } + + if (is_string($logical)) { + $logical = mb_strtoupper($logical, 'UTF-8'); + if (($logical == 'TRUE') || ($logical == Calculation::getTRUE())) { + return false; + } elseif (($logical == 'FALSE') || ($logical == Calculation::getFALSE())) { + return true; + } + + return ExcelError::VALUE(); + } + + return !$logical; + } + + /** + * @param mixed[] $args + * @param callable(int, int): bool $func + */ + private static function countTrueValues(array $args, callable $func): bool|string + { + $trueValueCount = 0; + $count = 0; + + $aArgs = Functions::flattenArrayIndexed($args); + foreach ($aArgs as $k => $arg) { + ++$count; + // Is it a boolean value? + if (is_bool($arg)) { + $trueValueCount += $arg; + } elseif (is_string($arg)) { + $isLiteral = !Functions::isCellValue($k); + $arg = mb_strtoupper($arg, 'UTF-8'); + if ($isLiteral && ($arg == 'TRUE' || $arg == Calculation::getTRUE())) { + ++$trueValueCount; + } elseif ($isLiteral && ($arg == 'FALSE' || $arg == Calculation::getFALSE())) { + //$trueValueCount += 0; + } else { + --$count; + } + } elseif (is_int($arg) || is_float($arg)) { + $trueValueCount += (int) ($arg != 0); + } else { + --$count; + } + } + + return ($count === 0) ? ExcelError::VALUE() : $func($trueValueCount, $count); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Address.php new file mode 100644 index 0000000..a17e554 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -0,0 +1,126 @@ + '') { + if (str_contains($sheetName, ' ') || str_contains($sheetName, '[')) { + $sheetName = "'{$sheetName}'"; + } + $sheetName .= '!'; + } + + return $sheetName; + } + + private static function formatAsA1(int $row, int $column, int $relativity, string $sheetName): string + { + $rowRelative = $columnRelative = '$'; + if (($relativity == self::ADDRESS_COLUMN_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $columnRelative = ''; + } + if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $rowRelative = ''; + } + $column = Coordinate::stringFromColumnIndex($column); + + return "{$sheetName}{$columnRelative}{$column}{$rowRelative}{$row}"; + } + + private static function formatAsR1C1(int $row, int $column, int $relativity, string $sheetName): string + { + if (($relativity == self::ADDRESS_COLUMN_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $column = "[{$column}]"; + } + if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $row = "[{$row}]"; + } + [$rowChar, $colChar] = AddressHelper::getRowAndColumnChars(); + + return "{$sheetName}$rowChar{$row}$colChar{$column}"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php new file mode 100644 index 0000000..944675e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php @@ -0,0 +1,251 @@ + [$x]) : null, ...$array)); // @phpstan-ignore-line + } + + /** @return mixed[] */ + private static function arrayValues(mixed $array): array + { + return is_array($array) ? array_values($array) : [$array]; + } + + /** + * CHOOSECOLS. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function chooseCols(mixed $input, mixed ...$args): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + $retval = self::chooseRows(self::transpose($input), ...$args); + + return is_array($retval) ? self::transpose($retval) : $retval; + } + + /** + * CHOOSEROWS. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function chooseRows(mixed $input, mixed ...$args): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + $inputArray = [[]]; // no row 0 + $numRows = 0; + foreach ($input as $inputRow) { + $inputArray[] = self::arrayValues($inputRow); + ++$numRows; + } + $outputArray = []; + foreach (Functions::flattenArray2(...$args) as $arg) { + if (!is_numeric($arg)) { + return ExcelError::VALUE(); + } + $index = (int) $arg; + if ($index < 0) { + $index += $numRows + 1; + } + if ($index <= 0 || $index > $numRows) { + return ExcelError::VALUE(); + } + $outputArray[] = $inputArray[$index]; + } + + return $outputArray; + } + + /** + * @param mixed[] $array + * + * @return mixed[]|string + */ + private static function dropRows(array $array, mixed $offset): array|string + { + if ($offset === null) { + return $array; + } + if (!is_numeric($offset)) { + return ExcelError::VALUE(); + } + $offset = (int) $offset; + $count = count($array); + if (abs($offset) >= $count) { + // In theory, this should be #CALC!, but Excel treats + // #CALC! as corrupt, and it's not worth figuring out why + return ExcelError::VALUE(); + } + if ($offset === 0) { + return $array; + } + if ($offset > 0) { + return array_slice($array, $offset); + } + + return array_slice($array, 0, $count + $offset); + } + + /** + * DROP. + * + * @param mixed $input expect two-dimensional array + * + * @return mixed[]|string + */ + public static function drop(mixed $input, mixed $rows = null, mixed $columns = null): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + $inputArray = []; // no row 0 + foreach ($input as $inputRow) { + $inputArray[] = self::arrayValues($inputRow); + } + $outputArray1 = self::dropRows($inputArray, $rows); + if (is_string($outputArray1)) { + return $outputArray1; + } + $outputArray2 = self::transpose($outputArray1); + $outputArray3 = self::dropRows($outputArray2, $columns); + if (is_string($outputArray3)) { + return $outputArray3; + } + + return self::transpose($outputArray3); + } + + /** + * @param mixed[] $array + * + * @return mixed[]|string + */ + private static function takeRows(array $array, mixed $offset): array|string + { + if ($offset === null) { + return $array; + } + if (!is_numeric($offset)) { + return ExcelError::VALUE(); + } + $offset = (int) $offset; + if ($offset === 0) { + // should be #CALC! - see above + return ExcelError::VALUE(); + } + $count = count($array); + if (abs($offset) >= $count) { + return $array; + } + if ($offset > 0) { + return array_slice($array, 0, $offset); + } + + return array_slice($array, $count + $offset); + } + + /** + * TAKE. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function take(mixed $input, mixed $rows, mixed $columns = null): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + if ($rows === null && $columns === null) { + return $input; + } + $inputArray = []; + foreach ($input as $inputRow) { + $inputArray[] = self::arrayValues($inputRow); + } + $outputArray1 = self::takeRows($inputArray, $rows); + if (is_string($outputArray1)) { + return $outputArray1; + } + $outputArray2 = self::transpose($outputArray1); + $outputArray3 = self::takeRows($outputArray2, $columns); + if (is_string($outputArray3)) { + return $outputArray3; + } + + return self::transpose($outputArray3); + } + + /** + * EXPAND. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function expand(mixed $input, mixed $rows, mixed $columns = null, mixed $pad = '#N/A'): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + if ($rows === null && $columns === null) { + return $input; + } + $numRows = count($input); + $rows ??= $numRows; + if (!is_numeric($rows)) { + return ExcelError::VALUE(); + } + $rows = (int) $rows; + if ($rows < count($input)) { + return ExcelError::VALUE(); + } + $numCols = 0; + foreach ($input as $inputRow) { + $numCols = max($numCols, is_array($inputRow) ? count($inputRow) : 1); + } + $columns ??= $numCols; + if (!is_numeric($columns)) { + return ExcelError::VALUE(); + } + $columns = (int) $columns; + if ($columns < $numCols) { + return ExcelError::VALUE(); + } + $inputArray = []; + foreach ($input as $inputRow) { + $inputArray[] = array_pad(self::arrayValues($inputRow), $columns, $pad); + } + $outputArray = []; + $padRow = array_pad([], $columns, $pad); + for ($count = 0; $count < $rows; ++$count) { + $outputArray[] = ($count >= $numRows) ? $padRow : $inputArray[$count]; + } + + return $outputArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php new file mode 100644 index 0000000..b7e0f9d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php @@ -0,0 +1,261 @@ +|float|int|string The relative position of the found item + */ + public static function MATCH(mixed $lookupValue, mixed $lookupArray, mixed $matchType = self::MATCHTYPE_LARGEST_VALUE): array|string|int|float + { + if (is_array($lookupValue)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType); + } + + $lookupArray = Functions::flattenArray($lookupArray); + + try { + // Input validation + self::validateLookupValue($lookupValue); + $matchType = self::validateMatchType($matchType); + self::validateLookupArray($lookupArray); + + $keySet = array_keys($lookupArray); + if ($matchType == self::MATCHTYPE_LARGEST_VALUE) { + // If match_type is 1 the list has to be processed from last to first + $lookupArray = array_reverse($lookupArray); + $keySet = array_reverse($keySet); + } + + $lookupArray = self::prepareLookupArray($lookupArray, $matchType); + } catch (Exception $e) { + return $e->getMessage(); + } + + // MATCH() is not case-sensitive, so we convert lookup value to be lower cased if it's a string type. + if (is_string($lookupValue)) { + $lookupValue = StringHelper::strToLower($lookupValue); + } + + $valueKey = match ($matchType) { + self::MATCHTYPE_LARGEST_VALUE => self::matchLargestValue($lookupArray, $lookupValue, $keySet), + self::MATCHTYPE_FIRST_VALUE => self::matchFirstValue($lookupArray, $lookupValue), + default => self::matchSmallestValue($lookupArray, $lookupValue), + }; + + if ($valueKey !== null) { + return ++$valueKey; //* @phpstan-ignore-line + } + + // Unsuccessful in finding a match, return #N/A error value + return ExcelError::NA(); + } + + /** @param mixed[] $lookupArray */ + private static function matchFirstValue(array $lookupArray, mixed $lookupValue): int|string|null + { + if (is_string($lookupValue)) { + $valueIsString = true; + $wildcard = WildcardMatch::wildcard($lookupValue); + } else { + $valueIsString = false; + $wildcard = ''; + } + + $valueIsNumeric = is_int($lookupValue) || is_float($lookupValue); + foreach ($lookupArray as $i => $lookupArrayValue) { + if ( + $valueIsString + && is_string($lookupArrayValue) + ) { + if (WildcardMatch::compare($lookupArrayValue, $wildcard)) { + return $i; // wildcard match + } + } else { + if ($lookupArrayValue === $lookupValue) { + return $i; // exact match + } + if ( + $valueIsNumeric + && (is_float($lookupArrayValue) || is_int($lookupArrayValue)) + && $lookupArrayValue == $lookupValue + ) { + return $i; // exact match + } + } + } + + return null; + } + + /** + * @param mixed[] $lookupArray + * @param mixed[] $keySet + */ + private static function matchLargestValue(array $lookupArray, mixed $lookupValue, array $keySet): mixed + { + if (is_string($lookupValue)) { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + $wildcard = WildcardMatch::wildcard($lookupValue); + foreach (array_reverse($lookupArray) as $i => $lookupArrayValue) { + if (is_string($lookupArrayValue) && WildcardMatch::compare($lookupArrayValue, $wildcard)) { + return $i; + } + } + } else { + foreach ($lookupArray as $i => $lookupArrayValue) { + if ($lookupArrayValue === $lookupValue) { + return $keySet[$i]; + } + } + } + } + $valueIsNumeric = is_int($lookupValue) || is_float($lookupValue); + foreach ($lookupArray as $i => $lookupArrayValue) { + if ($valueIsNumeric && (is_int($lookupArrayValue) || is_float($lookupArrayValue))) { + if ($lookupArrayValue <= $lookupValue) { + return array_search($i, $keySet); + } + } + $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + if ($typeMatch && ($lookupArrayValue <= $lookupValue)) { + return array_search($i, $keySet); + } + } + + return null; + } + + /** @param mixed[] $lookupArray */ + private static function matchSmallestValue(array $lookupArray, mixed $lookupValue): int|string|null + { + $valueKey = null; + if (is_string($lookupValue)) { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + $wildcard = WildcardMatch::wildcard($lookupValue); + foreach ($lookupArray as $i => $lookupArrayValue) { + if (is_string($lookupArrayValue) && WildcardMatch::compare($lookupArrayValue, $wildcard)) { + return $i; + } + } + } + } + + $valueIsNumeric = is_int($lookupValue) || is_float($lookupValue); + // The basic algorithm is: + // Iterate and keep the highest match until the next element is smaller than the searched value. + // Return immediately if perfect match is found + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + $bothNumeric = $valueIsNumeric && (is_int($lookupArrayValue) || is_float($lookupArrayValue)); + + if ($lookupArrayValue === $lookupValue) { + // Another "special" case. If a perfect match is found, + // the algorithm gives up immediately + return $i; + } + if ($bothNumeric && $lookupValue == $lookupArrayValue) { + return $i; // exact match, as above + } + if (($typeMatch || $bothNumeric) && $lookupArrayValue >= $lookupValue) { + $valueKey = $i; + } elseif ($typeMatch && $lookupArrayValue < $lookupValue) { + //Excel algorithm gives up immediately if the first element is smaller than the searched value + break; + } + } + + return $valueKey; + } + + private static function validateLookupValue(mixed $lookupValue): void + { + // Lookup_value type has to be number, text, or logical values + if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) { + throw new Exception(ExcelError::NA()); + } + } + + private static function validateMatchType(mixed $matchType): int + { + // Match_type is 0, 1 or -1 + // However Excel accepts other numeric values, + // including numeric strings and floats. + // It seems to just be interested in the sign. + if (!is_numeric($matchType)) { + throw new Exception(ExcelError::Value()); + } + if ($matchType > 0) { + return self::MATCHTYPE_LARGEST_VALUE; + } + if ($matchType < 0) { + return self::MATCHTYPE_SMALLEST_VALUE; + } + + return self::MATCHTYPE_FIRST_VALUE; + } + + /** @param mixed[] $lookupArray */ + private static function validateLookupArray(array $lookupArray): void + { + // Lookup_array should not be empty + $lookupArraySize = count($lookupArray); + if ($lookupArraySize <= 0) { + throw new Exception(ExcelError::NA()); + } + } + + /** + * @param mixed[] $lookupArray + * + * @return mixed[] + */ + private static function prepareLookupArray(array $lookupArray, mixed $matchType): array + { + // Lookup_array should contain only number, text, or logical values, or empty (null) cells + foreach ($lookupArray as $i => $value) { + // check the type of the value + if ((!is_numeric($value)) && (!is_string($value)) && (!is_bool($value)) && ($value !== null)) { + throw new Exception(ExcelError::NA()); + } + // Convert strings to lowercase for case-insensitive testing + if (is_string($value)) { + $lookupArray[$i] = StringHelper::strToLower($value); + } + if ( + ($value === null) + && (($matchType == self::MATCHTYPE_LARGEST_VALUE) || ($matchType == self::MATCHTYPE_SMALLEST_VALUE)) + ) { + unset($lookupArray[$i]); + } + } + + return $lookupArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php new file mode 100644 index 0000000..486194e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -0,0 +1,95 @@ + (bool) ($matchArray[$index] ?? null), + ARRAY_FILTER_USE_KEY + ); + } + + /** + * @param mixed[] $lookupArray + * @param mixed[] $matchArray + * + * @return mixed[] + */ + private static function filterByColumn(array $lookupArray, array $matchArray): array + { + $lookupArray = Matrix::transpose($lookupArray); + + if (count($matchArray) === 1) { + $matchArray = array_pop($matchArray); + } + /** @var mixed[] $matchArray */ + array_walk( + $matchArray, + function (&$value): void { + $value = [$value]; + } + ); + + $result = self::filterByRow($lookupArray, $matchArray); + + return Matrix::transpose($result); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php new file mode 100644 index 0000000..f4982a0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Formula.php @@ -0,0 +1,44 @@ +getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName) + : $cell->getWorksheet(); + } + + if ( + $worksheet === null + || !$worksheet->cellExists($cellReference) + || !$worksheet->getCell($cellReference)->isFormula() + ) { + return ExcelError::NA(); + } + + return $worksheet->getCell($cellReference)->getValueString(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php new file mode 100644 index 0000000..30c021b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -0,0 +1,130 @@ +|float|int|string $indexNumber The row number in table_array from which the matching value must be returned. + * The first row is 1. + * @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value + * + * @return mixed The value of the found cell + */ + public static function lookup(mixed $lookupValue, $lookupArray, $indexNumber, mixed $notExactMatch = true): mixed + { + if (is_array($lookupValue) || is_array($indexNumber)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch); + } + + $notExactMatch = (bool) ($notExactMatch ?? true); + + try { + self::validateLookupArray($lookupArray); + $lookupArray = self::convertLiteralArray($lookupArray); + $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); + } catch (Exception $e) { + return $e->getMessage(); + } + + $f = array_keys($lookupArray); + $firstRow = reset($f); + if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray))) { + return ExcelError::REF(); + } + + $firstkey = $f[0] - 1; + $returnColumn = $firstkey + $indexNumber; + /** @var mixed[][] $lookupArray */ + $firstColumn = array_shift($f) ?? 1; + $rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); + + if ($rowNumber !== null) { + // otherwise return the appropriate value + return $lookupArray[$returnColumn][Coordinate::stringFromColumnIndex($rowNumber)]; + } + + return ExcelError::NA(); + } + + /** + * @param mixed $lookupValue The value that you want to match in lookup_array + * @param mixed[][] $lookupArray + * @param int|string $column + */ + private static function hLookupSearch(mixed $lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int + { + $lookupLower = StringHelper::strToLower(StringHelper::convertToString($lookupValue)); + + $rowNumber = null; + foreach ($lookupArray[$column] as $rowKey => $rowData) { + // break if we have passed possible keys + /** @var string $rowKey */ + $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData); + $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData); + /** @var scalar $rowData */ + $cellDataLower = StringHelper::strToLower((string) $rowData); + + if ( + $notExactMatch + && (($bothNumeric && $rowData > $lookupValue) || ($bothNotNumeric && $cellDataLower > $lookupLower)) + ) { + break; + } + + $rowNumber = self::checkMatch( + $bothNumeric, + $bothNotNumeric, + $notExactMatch, + Coordinate::columnIndexFromString($rowKey), + $cellDataLower, + $lookupLower, + $rowNumber + ); + } + + return $rowNumber; + } + + /** + * @param mixed[] $lookupArray + * + * @return mixed[] + */ + private static function convertLiteralArray(array $lookupArray): array + { + if (array_key_exists(0, $lookupArray)) { + $lookupArray2 = []; + $row = 0; + foreach ($lookupArray as $arrayVal) { + ++$row; + if (!is_array($arrayVal)) { + $arrayVal = [$arrayVal]; + } + $arrayVal2 = []; + foreach ($arrayVal as $key2 => $val2) { + $index = Coordinate::stringFromColumnIndex($key2 + 1); + $arrayVal2[$index] = $val2; + } + $lookupArray2[$row] = $arrayVal2; + } + $lookupArray = $lookupArray2; + } + + return $lookupArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php new file mode 100644 index 0000000..5f94ffa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -0,0 +1,75 @@ +getWorkSheet(); + $sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle(); + $value = (string) preg_replace('/^=/', '', $namedRange->getValue()); + self::adjustSheetTitle($sheetTitle, $value); + $cellAddress1 = $sheetTitle . $value; + $cellAddress = $cellAddress1; + $a1 = self::CELLADDRESS_USE_A1; + } + if (str_contains($cellAddress, ':')) { + [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); + } + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol); + + return [$cellAddress1, $cellAddress2, $cellAddress]; + } + + /** @return array{string, ?Worksheet, string} */ + public static function extractWorksheet(string $cellAddress, Cell $cell): array + { + $sheetName = ''; + if (str_contains($cellAddress, '!')) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true, true); + } + + $worksheet = ($sheetName !== '') + ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($sheetName) + : $cell->getWorksheet(); + + return [$cellAddress, $worksheet, $sheetName]; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php new file mode 100644 index 0000000..9be77e5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php @@ -0,0 +1,48 @@ + $rows) { + $matrix[] = array_pad([], $columns, ExcelError::NA()); + ++$rows; + } + } + + $transpose = array_map(null, ...$inputData); //* @phpstan-ignore-line + $returnMatrix = []; + foreach ($transpose as $array) { + $returnMatrix[] = Functions::flattenArray($array); + } + + return $returnMatrix; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php new file mode 100644 index 0000000..e7752aa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php @@ -0,0 +1,47 @@ +getHyperlink() + ->setUrl($linkURL); + $cell->getHyperlink()->setTooltip($displayName); + + return $displayName; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php new file mode 100644 index 0000000..ea8db54 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -0,0 +1,130 @@ +getCoordinate()); + + try { + $a1 = self::a1Format($a1fmt); + $cellAddress = self::validateAddress($cellAddress); + } catch (Exception $e) { + return $e->getMessage(); + } + + [$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); + + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } + + try { + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol); + } catch (Exception) { + return ExcelError::REF(); + } + + if ( + (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) + || (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress2, $matches))) + ) { + return ExcelError::REF(); + } + + return self::extractRequiredCells($worksheet, $cellAddress); + } + + /** + * Extract range values. + * + * @return mixed[] Array of values in range if range contains more than one element. + * Otherwise, a single value is returned. + */ + private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array + { + return Calculation::getInstance($worksheet?->getParent()) + ->extractCellRange($cellAddress, $worksheet, false, createCell: true); + } + + private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string + { + // Being lazy, we're only checking a single row/column to get the max + if (ctype_digit($start) && $start <= 1048576) { + // Max 16,384 columns for Excel2007 + $endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : AddressRange::MAX_COLUMN; + + return "A{$start}:{$endColRef}{$end}"; + } elseif (ctype_alpha($start) && strlen($start) <= 3) { + // Max 1,048,576 rows for Excel2007 + $endRowRef = ($worksheet !== null) ? $worksheet->getHighestDataRow($start) : AddressRange::MAX_ROW; + + return "{$start}1:{$end}{$endRowRef}"; + } + + return "{$start}:{$end}"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php new file mode 100644 index 0000000..36472c2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php @@ -0,0 +1,123 @@ + 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) { + $lookupVector = Matrix::transpose($lookupVector); + $lookupRows = self::rowCount($lookupVector); + /** @var mixed[][] $lookupVector */ + $lookupColumns = self::columnCount($lookupVector); + } + + $resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore-line + + if ($lookupRows === 2 && !$hasResultVector) { + $resultVector = array_pop($lookupVector); + $lookupVector = array_shift($lookupVector); + } + + /** @var array $lookupVector */ + /** @var array $resultVector */ + if ($lookupColumns !== 2) { + $lookupVector = self::verifyLookupValues($lookupVector, $resultVector); + } + + return VLookup::lookup($lookupValue, $lookupVector, 2); + } + + /** + * @param array $lookupVector + * @param array $resultVector + * + * @return mixed[] + */ + private static function verifyLookupValues(array $lookupVector, array $resultVector): array + { + foreach ($lookupVector as &$value) { + if (is_array($value)) { + $k = array_keys($value); + $key1 = $key2 = array_shift($k); + ++$key2; + $dataValue1 = $value[$key1]; + } else { + $key1 = 0; + $key2 = 1; + $dataValue1 = $value; + } + + $dataValue2 = array_shift($resultVector); + if (is_array($dataValue2)) { + $dataValue2 = array_shift($dataValue2); + } + /** @var int $key2 */ + $value = [$key1 => $dataValue1, $key2 => $dataValue2]; + } + unset($value); + + return $lookupVector; + } + + /** + * @param mixed[][] $resultVector + * + * @return mixed[] + */ + private static function verifyResultVector(array $resultVector): array + { + $resultRows = self::rowCount($resultVector); + $resultColumns = self::columnCount($resultVector); + + // we correctly orient our results + if ($resultRows === 1 && $resultColumns > 1) { + $resultVector = Matrix::transpose($resultVector); + } + + return $resultVector; + } + + /** @param mixed[] $dataArray */ + private static function rowCount(array $dataArray): int + { + return count($dataArray); + } + + /** @param mixed[][] $dataArray */ + private static function columnCount(array $dataArray): int + { + $rowKeys = array_keys($dataArray); + $row = array_shift($rowKeys); + + return count($dataArray[$row]); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php new file mode 100644 index 0000000..39c498f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -0,0 +1,67 @@ += 1 + */ + protected static function validateIndexLookup(array $lookupArray, $index_number): int + { + // index_number must be a number greater than or equal to 1. + // Excel results are inconsistent when index is non-numeric. + // VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but + // VLOOKUP(whatever, whatever, cellref) yields REF error + // when cellref is '=SQRT(-1)'. So just try our best here. + // Similar results if string (literal yields VALUE, cellRef REF). + if (!is_numeric($index_number)) { + throw new Exception(ExcelError::throwError($index_number)); + } + if ($index_number < 1) { + throw new Exception(ExcelError::VALUE()); + } + + // index_number must be less than or equal to the number of columns in lookupArray + if (empty($lookupArray)) { + throw new Exception(ExcelError::REF()); + } + + return (int) $index_number; + } + + protected static function checkMatch( + bool $bothNumeric, + bool $bothNotNumeric, + bool $notExactMatch, + int $rowKey, + string $cellDataLower, + string $lookupLower, + ?int $rowNumber + ): ?int { + // remember the last key, but only if datatypes match + if ($bothNumeric || $bothNotNumeric) { + // Spreadsheets software returns first exact match, + // we have sorted and we might have broken key orders + // we want the first one (by its initial index) + if ($notExactMatch) { + $rowNumber = $rowKey; + } elseif (($cellDataLower == $lookupLower) && (($rowNumber === null) || ($rowKey < $rowNumber))) { + $rowNumber = $rowKey; + } + } + + return $rowNumber; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php new file mode 100644 index 0000000..620705e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php @@ -0,0 +1,34 @@ + 1 + && (count($values, COUNT_NORMAL) === 1 || count($values, COUNT_RECURSIVE) === count($values, COUNT_NORMAL)); + } + + /** + * TRANSPOSE. + * + * @param mixed $matrixData A matrix of values + * + * @return mixed[] + */ + public static function transpose($matrixData): array + { + $returnMatrix = []; + if (!is_array($matrixData)) { + $matrixData = [[$matrixData]]; + } + if (!is_array(end($matrixData))) { + $matrixData = [$matrixData]; + } + + $column = 0; + /** @var mixed[][] $matrixData */ + foreach ($matrixData as $matrixRow) { + $row = 0; + foreach ($matrixRow as $matrixCell) { + $returnMatrix[$row][$column] = $matrixCell; + ++$row; + } + ++$column; + } + + return $returnMatrix; + } + + /** + * INDEX. + * + * Uses an index to choose a value from a reference or array + * + * Excel Function: + * =INDEX(range_array, row_num, [column_num], [area_num]) + * + * @param mixed $matrix A range of cells or an array constant + * @param mixed $rowNum The row in the array or range from which to return a value. + * If row_num is omitted, column_num is required. + * Or can be an array of values + * @param mixed $columnNum The column in the array or range from which to return a value. + * If column_num is omitted, row_num is required. + * Or can be an array of values + * + * TODO Provide support for area_num, currently not supported + * + * @return mixed the value of a specified cell or array of cells + * If an array of values is passed as the $rowNum and/or $columnNum arguments, then the returned result + * will also be an array with the same dimensions + */ + public static function index(mixed $matrix, mixed $rowNum = 0, mixed $columnNum = null): mixed + { + if (is_array($rowNum) || is_array($columnNum)) { + return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $matrix, $rowNum, $columnNum); + } + + $rowNum = $rowNum ?? 0; + $columnNum = $columnNum ?? 0; + if (is_scalar($matrix)) { + if ($rowNum === 0 || $rowNum === 1) { + if ($columnNum === 0 || $columnNum === 1) { + if ($columnNum === 1 || $rowNum === 1) { + return $matrix; + } + } + } + } + + try { + $rowNum = LookupRefValidations::validatePositiveInt($rowNum); + $columnNum = LookupRefValidations::validatePositiveInt($columnNum); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (is_array($matrix) && count($matrix) === 1 && $rowNum > 1) { + $matrixKey = array_keys($matrix)[0]; + if (is_array($matrix[$matrixKey])) { + $tempMatrix = []; + foreach ($matrix[$matrixKey] as $key => $value) { + $tempMatrix[$key] = [$value]; + } + $matrix = $tempMatrix; + } + } + + if (!is_array($matrix) || ($rowNum > count($matrix))) { + return ExcelError::REF(); + } + + $rowKeys = array_keys($matrix); + $columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore-line + + if ($columnNum > count($columnKeys)) { + return ExcelError::REF(); + } + + if ($columnNum === 0) { + return self::extractRowValue($matrix, $rowKeys, $rowNum); + } + + $columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore-line + if ($rowNum === 0) { + return array_map( + fn ($value): array => [$value], + array_column($matrix, $columnNum) + ); + } + $rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore-line + /** @var mixed[][] $matrix */ + + return $matrix[$rowNum][$columnNum]; + } + + /** + * @param mixed[] $matrix + * @param array $rowKeys + */ + private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum): mixed + { + if ($rowNum === 0) { + return $matrix; + } + + $rowNum = $rowKeys[--$rowNum]; + $row = $matrix[$rowNum]; + if (is_array($row)) { + return [$rowNum => $row]; + } + + return $row; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php new file mode 100644 index 0000000..bc6537d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -0,0 +1,165 @@ +|string An array containing a cell or range of cells, or a string on error + */ + public static function OFFSET(?string $cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, ?Cell $cell = null): string|array + { + /** @var int */ + $rows = Functions::flattenSingleValue($rows); + /** @var int */ + $columns = Functions::flattenSingleValue($columns); + /** @var int */ + $height = Functions::flattenSingleValue($height); + /** @var int */ + $width = Functions::flattenSingleValue($width); + + if ($cellAddress === null || $cellAddress === '') { + return ExcelError::VALUE(); + } + + if (!is_object($cell)) { + return ExcelError::REF(); + } + $sheet = $cell->getParent()?->getParent(); // worksheet + if ($sheet !== null) { + $cellAddress = Validations::definedNameToCoordinate($cellAddress, $sheet); + } + + [$cellAddress, $worksheet] = self::extractWorksheet($cellAddress, $cell); + + $startCell = $endCell = $cellAddress; + if (strpos($cellAddress, ':')) { + [$startCell, $endCell] = explode(':', $cellAddress); + } + [$startCellColumn, $startCellRow] = Coordinate::indexesFromString($startCell); + [, $endCellRow, $endCellColumn] = Coordinate::indexesFromString($endCell); + + $startCellRow += $rows; + $startCellColumn += $columns - 1; + + if (($startCellRow <= 0) || ($startCellColumn < 0)) { + return ExcelError::REF(); + } + + $endCellColumn = self::adjustEndCellColumnForWidth($endCellColumn, $width, $startCellColumn, $columns); + $startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1); + + $endCellRow = self::adjustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow); + + if (($endCellRow <= 0) || ($endCellColumn < 0)) { + return ExcelError::REF(); + } + $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1); + + $cellAddress = "{$startCellColumn}{$startCellRow}"; + if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) { + $cellAddress .= ":{$endCellColumn}{$endCellRow}"; + } + + return self::extractRequiredCells($worksheet, $cellAddress); + } + + /** @return mixed[] */ + private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array + { + return Calculation::getInstance($worksheet?->getParent()) + ->extractCellRange($cellAddress, $worksheet, false); + } + + /** @return array{string, ?Worksheet} */ + private static function extractWorksheet(?string $cellAddress, Cell $cell): array + { + $cellAddress = self::assessCellAddress($cellAddress ?? '', $cell); + + $sheetName = ''; + if (str_contains($cellAddress, '!')) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true, true); + } + + $worksheet = ($sheetName !== '') + ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($sheetName) + : $cell->getWorksheet(); + + return [$cellAddress, $worksheet]; + } + + private static function assessCellAddress(string $cellAddress, Cell $cell): string + { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $cellAddress) !== false) { + $cellAddress = Functions::expandDefinedName($cellAddress, $cell); + } + + return $cellAddress; + } + + /** + * @param null|object|scalar $width + * @param scalar $columns + */ + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns): int + { + $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; + if (($width !== null) && (!is_object($width))) { + $endCellColumn = $startCellColumn + (int) $width - 1; + } else { + $endCellColumn += (int) $columns; + } + + return $endCellColumn; + } + + /** + * @param null|object|scalar $height + * @param scalar $rows + */ + private static function adjustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int + { + if (($height !== null) && (!is_object($height))) { + $endCellRow = $startCellRow + (int) $height - 1; + } else { + $endCellRow += (int) $rows; + } + + return $endCellRow; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php new file mode 100644 index 0000000..caf0983 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -0,0 +1,231 @@ +getColumn()) : 1; + } + + /** + * COLUMN. + * + * Returns the column number of the given cell reference + * If the cell reference is a range of cells, COLUMN returns the column numbers of each column + * in the reference as a horizontal array. + * If cell reference is omitted, and the function is being called through the calculation engine, + * then it is assumed to be the reference of the cell in which the COLUMN function appears; + * otherwise this function returns 1. + * + * Excel Function: + * =COLUMN([cellAddress]) + * + * @param null|mixed[]|string $cellAddress A reference to a range of cells for which you want the column numbers + * + * @return int|int[]|string + */ + public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|string|array + { + if (self::cellAddressNullOrWhitespace($cellAddress)) { + return self::cellColumn($cell); + } + + if (is_array($cellAddress)) { + foreach ($cellAddress as $columnKey => $value) { + $columnKey = (string) preg_replace('/[^a-z]/i', '', $columnKey); + + return Coordinate::columnIndexFromString($columnKey); + } + + return self::cellColumn($cell); + } + + $cellAddress = $cellAddress ?? ''; + if ($cell != null) { + [,, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); + [,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $cell->getWorksheet(), $sheetName); + } + [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $cellAddress ??= ''; + + if (str_contains($cellAddress, ':')) { + [$startAddress, $endAddress] = explode(':', $cellAddress); + $startAddress = (string) preg_replace('/[^a-z]/i', '', $startAddress); + $endAddress = (string) preg_replace('/[^a-z]/i', '', $endAddress); + + return range( + Coordinate::columnIndexFromString($startAddress), + Coordinate::columnIndexFromString($endAddress) + ); + } + + $cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress); + + try { + return Coordinate::columnIndexFromString($cellAddress); + } catch (SpreadsheetException) { + return ExcelError::NAME(); + } + } + + /** + * COLUMNS. + * + * Returns the number of columns in an array or reference. + * + * Excel Function: + * =COLUMNS(cellAddress) + * + * @param null|mixed[]|string $cellAddress An array or array formula, or a reference to a range of cells + * for which you want the number of columns + * + * @return int|string The number of columns in cellAddress, or a string if arguments are invalid + */ + public static function COLUMNS($cellAddress = null) + { + if (self::cellAddressNullOrWhitespace($cellAddress)) { + return 1; + } + if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) { + return $cellAddress; + } + if (!is_array($cellAddress)) { + return ExcelError::VALUE(); + } + + reset($cellAddress); + $isMatrix = (is_numeric(key($cellAddress))); + [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); + + if ($isMatrix) { + return $rows; + } + + return $columns; + } + + private static function cellRow(?Cell $cell): int|string + { + return ($cell !== null) ? self::convert0ToName($cell->getRow()) : 1; + } + + private static function convert0ToName(int|string $result): int|string + { + if (is_int($result) && ($result <= 0 || $result > 1048576)) { + return ExcelError::NAME(); + } + + return $result; + } + + /** + * ROW. + * + * Returns the row number of the given cell reference + * If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference + * as a vertical array. + * If cell reference is omitted, and the function is being called through the calculation engine, + * then it is assumed to be the reference of the cell in which the ROW function appears; + * otherwise this function returns 1. + * + * Excel Function: + * =ROW([cellAddress]) + * + * @param null|mixed[][]|string $cellAddress A reference to a range of cells for which you want the row numbers + * + * @return int|mixed[]|string + */ + public static function ROW($cellAddress = null, ?Cell $cell = null): int|string|array + { + if (self::cellAddressNullOrWhitespace($cellAddress)) { + return self::cellRow($cell); + } + + if (is_array($cellAddress)) { + foreach ($cellAddress as $rowKey => $rowValue) { + foreach ($rowValue as $columnKey => $cellValue) { + return (int) preg_replace('/\D/', '', $rowKey); + } + } + + return self::cellRow($cell); + } + + $cellAddress = $cellAddress ?? ''; + if ($cell !== null) { + [,, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); + [,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $cell->getWorksheet(), $sheetName); + } + [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $cellAddress ??= ''; + if (str_contains($cellAddress, ':')) { + [$startAddress, $endAddress] = explode(':', $cellAddress); + $startAddress = (int) (string) preg_replace('/\D/', '', $startAddress); + $endAddress = (int) (string) preg_replace('/\D/', '', $endAddress); + + return array_map( + fn ($value): array => [$value], + range($startAddress, $endAddress) + ); + } + [$cellAddress] = explode(':', $cellAddress); + + return self::convert0ToName((int) preg_replace('/\D/', '', $cellAddress)); + } + + /** + * ROWS. + * + * Returns the number of rows in an array or reference. + * + * Excel Function: + * =ROWS(cellAddress) + * + * @param null|mixed[]|string $cellAddress An array or array formula, or a reference to a range of cells + * for which you want the number of rows + * + * @return int|string The number of rows in cellAddress, or a string if arguments are invalid + */ + public static function ROWS($cellAddress = null) + { + if (self::cellAddressNullOrWhitespace($cellAddress)) { + return 1; + } + if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) { + return $cellAddress; + } + if (!is_array($cellAddress)) { + return ExcelError::VALUE(); + } + + reset($cellAddress); + $isMatrix = (is_numeric(key($cellAddress))); + [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); + + if ($isMatrix) { + return $columns; + } + + return $rows; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php new file mode 100644 index 0000000..8d8daee --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php @@ -0,0 +1,51 @@ + $entryCount)) { + return ExcelError::VALUE(); + } + + if (is_array($chooseArgs[$chosenEntry])) { + return Functions::flattenArray($chooseArgs[$chosenEntry]); + } + + return $chooseArgs[$chosenEntry]; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php new file mode 100644 index 0000000..b640f7d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php @@ -0,0 +1,353 @@ +getMessage(); + } + + // We want a simple, enumerated array of arrays where we can reference column by its index number. + /** @var callable(mixed): mixed */ + $temp = 'array_values'; + /** @var array $sortOrder */ + $sortArray = array_values(array_map($temp, $sortArray)); + /** @var int[] $sortIndex */ + + return ($byColumn === true) + ? self::sortByColumn($sortArray, $sortIndex, $sortOrder) + : self::sortByRow($sortArray, $sortIndex, $sortOrder); + } + + /** + * SORTBY + * The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array. + * The returned array is the same shape as the provided array argument. + * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting. + * + * @param mixed $sortArray The range of cells being sorted + * @param mixed $args + * At least one additional argument must be provided, The vector or range to sort on + * After that, arguments are passed as pairs: + * sort order: ascending or descending + * Ascending = 1 (self::ORDER_ASCENDING) + * Descending = -1 (self::ORDER_DESCENDING) + * additional arrays or ranges for multi-level sorting + * + * @return mixed The sorted values from the sort range + */ + public static function sortBy(mixed $sortArray, mixed ...$args): mixed + { + if (!is_array($sortArray)) { + // Scalars are always returned "as is" + return $sortArray; + } + + $sortArray = self::enumerateArrayKeys($sortArray); + + $lookupArraySize = count($sortArray); + $argumentCount = count($args); + + try { + $sortBy = $sortOrder = []; + for ($i = 0; $i < $argumentCount; $i += 2) { + $sortBy[] = self::validateSortVector($args[$i], $lookupArraySize); + $sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::processSortBy($sortArray, $sortBy, $sortOrder); + } + + /** + * @param mixed[] $sortArray + * + * @return mixed[] + */ + private static function enumerateArrayKeys(array $sortArray): array + { + array_walk( + $sortArray, + function (&$columns): void { + if (is_array($columns)) { + $columns = array_values($columns); + } + } + ); + + return array_values($sortArray); + } + + private static function validateScalarArgumentsForSort(mixed &$sortIndex, mixed &$sortOrder, int $sortArraySize): void + { + if (is_array($sortIndex) || is_array($sortOrder)) { + throw new Exception(ExcelError::VALUE()); + } + + $sortIndex = self::validatePositiveInt($sortIndex, false); + + if ($sortIndex > $sortArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + $sortOrder = self::validateSortOrder($sortOrder); + } + + /** @return mixed[] */ + private static function validateSortVector(mixed $sortVector, int $sortArraySize): array + { + if (!is_array($sortVector)) { + throw new Exception(ExcelError::VALUE()); + } + + // It doesn't matter if it's a row or a column vectors, it works either way + $sortVector = Functions::flattenArray($sortVector); + if (count($sortVector) !== $sortArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortVector; + } + + private static function validateSortOrder(mixed $sortOrder): int + { + $sortOrder = self::validateInt($sortOrder); + if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortOrder; + } + + /** @param mixed[] $sortIndex */ + private static function validateArrayArgumentsForSort(array &$sortIndex, mixed &$sortOrder, int $sortArraySize): void + { + // It doesn't matter if they're row or column vectors, it works either way + $sortIndex = Functions::flattenArray($sortIndex); + $sortOrder = Functions::flattenArray($sortOrder); + + if ( + count($sortOrder) === 0 || count($sortOrder) > $sortArraySize + || (count($sortOrder) > count($sortIndex)) + ) { + throw new Exception(ExcelError::VALUE()); + } + + if (count($sortIndex) > count($sortOrder)) { + // If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated. + $sortOrder = array_merge( + $sortOrder, + array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder)) + ); + } + + foreach ($sortIndex as $key => &$value) { + self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize); + } + } + + /** + * @param mixed[] $sortVector + * + * @return mixed[] + */ + private static function prepareSortVectorValues(array $sortVector): array + { + // Strings should be sorted case-insensitive; with booleans converted to locale-strings + return array_map( + function ($value) { + if (is_bool($value)) { + return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + } elseif (is_string($value)) { + return StringHelper::strToLower($value); + } + + return $value; + }, + $sortVector + ); + } + + /** + * @param mixed[] $sortArray + * @param mixed[] $sortIndex + * @param int[] $sortOrder + * + * @return mixed[] + */ + private static function processSortBy(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArguments = []; + /** @var mixed[] */ + $sortData = []; + foreach ($sortIndex as $index => $sortValues) { + /** @var mixed[] $sortValues */ + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + + $sortVector = self::executeVectorSortQuery($sortData, $sortArguments); + + return self::sortLookupArrayFromVector($sortArray, $sortVector); + } + + /** + * @param mixed[] $sortArray + * @param int[] $sortIndex + * @param int[] $sortOrder + * + * @return mixed[] + */ + private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder); + + return self::sortLookupArrayFromVector($sortArray, $sortVector); + } + + /** + * @param mixed[] $sortArray + * @param int[] $sortIndex + * @param int[] $sortOrder + * + * @return mixed[] + */ + private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArray = Matrix::transpose($sortArray); + $result = self::sortByRow($sortArray, $sortIndex, $sortOrder); + + return Matrix::transpose($result); + } + + /** + * @param mixed[] $sortArray + * @param int[] $sortIndex + * @param int[] $sortOrder + * + * @return mixed[] + */ + private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortIndexValue) { + $sortValues = array_column($sortArray, $sortIndexValue - 1); + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + + $sortData = self::executeVectorSortQuery($sortData, $sortArguments); + + return $sortData; + } + + /** + * @param mixed[] $sortData + * @param mixed[] $sortArguments + * + * @return mixed[] + */ + private static function executeVectorSortQuery(array $sortData, array $sortArguments): array + { + $sortData = Matrix::transpose($sortData); + + // We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys. + $sortDataIndexed = []; + foreach ($sortData as $key => $value) { + $sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value; + } + unset($sortData); + + $sortArguments[] = &$sortDataIndexed; + + array_multisort(...$sortArguments); + + // After the sort, we restore the numeric keys that will now be in the correct, sorted order + $sortedData = []; + foreach (array_keys($sortDataIndexed) as $key) { + $sortedData[] = Coordinate::columnIndexFromString($key) - 1; + } + + return $sortedData; + } + + /** + * @param mixed[] $sortArray + * @param mixed[] $sortVector + * + * @return mixed[] + */ + private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array + { + // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays + $sortedArray = []; + foreach ($sortVector as $index) { + /** @var int|string $index */ + $sortedArray[] = $sortArray[$index]; + } + + return $sortedArray; + +// uksort( +// $lookupArray, +// function (int $a, int $b) use (array $sortVector) { +// return $sortVector[$a] <=> $sortVector[$b]; +// } +// ); +// +// return $lookupArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php new file mode 100644 index 0000000..60b40d9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/TorowTocol.php @@ -0,0 +1,94 @@ + [$x]), $result); + } + + return $result; + } + + /** + * Excel function TOROW. + * + * @return mixed[]|string + */ + public static function torow(mixed $array, mixed $ignore = 0, mixed $byColumn = false): array|string + { + if (!is_numeric($ignore)) { + return ExcelError::VALUE(); + } + $ignore = (int) $ignore; + if ($ignore < 0 || $ignore > 3) { + return ExcelError::VALUE(); + } + if (is_int($byColumn) || is_float($byColumn)) { + $byColumn = (bool) $byColumn; + } + if (!is_bool($byColumn)) { + return ExcelError::VALUE(); + } + if (!is_array($array)) { + $array = [$array]; + } + if ($byColumn) { + $temp = []; + foreach ($array as $row) { + if (!is_array($row)) { + $row = [$row]; + } + $temp[] = Functions::flattenArray($row); + } + $array = ChooseRowsEtc::transpose($temp); + } else { + $array = Functions::flattenArray($array); + } + + return self::byRow($array, $ignore); + } + + /** + * @param mixed[] $array + * + * @return mixed[] + */ + private static function byRow(array $array, int $ignore): array + { + $returnMatrix = []; + foreach ($array as $row) { + if (!is_array($row)) { + $row = [$row]; + } + foreach ($row as $cell) { + if ($cell === null) { + if ($ignore === 1 || $ignore === 3) { + continue; + } + $cell = 0; + } elseif (ErrorValue::isError($cell)) { + if ($ignore === 2 || $ignore === 3) { + continue; + } + } + $returnMatrix[] = $cell; + } + } + + return $returnMatrix; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php new file mode 100644 index 0000000..40a9df7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php @@ -0,0 +1,164 @@ + count($flattenedLookupVector, COUNT_RECURSIVE) + 1) { + // We're looking at a full column check (multiple rows) + $transpose = Matrix::transpose($lookupVector); + $result = self::uniqueByRow($transpose, $exactlyOnce); + + return (is_array($result)) ? Matrix::transpose($result) : $result; + } + + $result = self::countValuesCaseInsensitive($flattenedLookupVector); + + if ($exactlyOnce === true) { + $result = self::exactlyOnceFilter($result); + } + + if (count($result) === 0) { + return ExcelError::CALC(); + } + + $result = array_keys($result); + + return $result; + } + + /** + * @param string[] $caseSensitiveLookupValues + * + * @return mixed[] + */ + private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array + { + $caseInsensitiveCounts = array_count_values( + array_map( + fn (string $value): string => StringHelper::strToUpper($value), + $caseSensitiveLookupValues + ) + ); + + $caseSensitiveCounts = []; + foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) { + if (is_numeric($caseInsensitiveKey)) { + $caseSensitiveCounts[$caseInsensitiveKey] = $count; + } else { + foreach ($caseSensitiveLookupValues as $caseSensitiveValue) { + if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) { + $caseSensitiveCounts[$caseSensitiveValue] = $count; + + break; + } + } + } + } + + return $caseSensitiveCounts; + } + + /** + * @param mixed[] $values + * + * @return mixed[] + */ + private static function exactlyOnceFilter(array $values): array + { + return array_filter( + $values, + fn ($value): bool => $value === 1 + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php new file mode 100644 index 0000000..76929c5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -0,0 +1,128 @@ +|float|int|string $indexNumber The column number in table_array from which the matching value must be returned. + * The first column is 1. + * @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value + * + * @return mixed The value of the found cell + */ + public static function lookup(mixed $lookupValue, $lookupArray, mixed $indexNumber, mixed $notExactMatch = true): mixed + { + if (is_array($lookupValue) || is_array($indexNumber)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch); + } + + $notExactMatch = (bool) ($notExactMatch ?? true); + + try { + self::validateLookupArray($lookupArray); + $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); + } catch (Exception $e) { + return $e->getMessage(); + } + + $f = array_keys($lookupArray); + $firstRow = array_pop($f); + if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray[$firstRow]))) { + return ExcelError::REF(); + } + $columnKeys = array_keys($lookupArray[$firstRow]); + $returnColumn = $columnKeys[--$indexNumber]; + $firstColumn = array_shift($columnKeys) ?? 1; + + if (!$notExactMatch) { + /** @var callable $callable */ + $callable = [self::class, 'vlookupSort']; + uasort($lookupArray, $callable); + } + + /** @var string[][] $lookupArray */ + $rowNumber = self::vLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); + + if ($rowNumber !== null) { + // return the appropriate value + return $lookupArray[$rowNumber][$returnColumn]; + } + + return ExcelError::NA(); + } + + /** + * @param scalar[] $a + * @param scalar[] $b + */ + private static function vlookupSort(array $a, array $b): int + { + reset($a); + $firstColumn = key($a); + $aLower = StringHelper::strToLower((string) $a[$firstColumn]); + $bLower = StringHelper::strToLower((string) $b[$firstColumn]); + + if ($aLower == $bLower) { + return 0; + } + + return ($aLower < $bLower) ? -1 : 1; + } + + /** + * @param mixed $lookupValue The value that you want to match in lookup_array + * @param string[][] $lookupArray + * @param int|string $column + */ + private static function vLookupSearch(mixed $lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int + { + $lookupLower = StringHelper::strToLower(StringHelper::convertToString($lookupValue)); + + $rowNumber = null; + foreach ($lookupArray as $rowKey => $rowData) { + $bothNumeric = self::numeric($lookupValue) && self::numeric($rowData[$column]); + $bothNotNumeric = !self::numeric($lookupValue) && !self::numeric($rowData[$column]); + $cellDataLower = StringHelper::strToLower((string) $rowData[$column]); + + // break if we have passed possible keys + if ( + $notExactMatch + && (($bothNumeric && ($rowData[$column] > $lookupValue)) + || ($bothNotNumeric && ($cellDataLower > $lookupLower))) + ) { + break; + } + + $rowNumber = self::checkMatch( + $bothNumeric, + $bothNotNumeric, + $notExactMatch, + $rowKey, + $cellDataLower, + $lookupLower, + $rowNumber + ); + } + + return $rowNumber; + } + + private static function numeric(mixed $value): bool + { + return is_int($value) || is_float($value); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php new file mode 100644 index 0000000..919c262 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php @@ -0,0 +1,42 @@ +|float|int|string rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate(mixed $number): array|string|int|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return abs($number); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php new file mode 100644 index 0000000..7c3597d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php @@ -0,0 +1,63 @@ +|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toDegrees(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return rad2deg($number); + } + + /** + * RADIANS. + * + * Returns the result of builtin function deg2rad after validating args. + * + * @param mixed $number Should be numeric, or can be an array of numbers + * + * @return array|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function toRadians(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return deg2rad($number); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php new file mode 100644 index 0000000..04549a1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php @@ -0,0 +1,97 @@ + 1000, + 'D' => 500, + 'C' => 100, + 'L' => 50, + 'X' => 10, + 'V' => 5, + 'I' => 1, + ]; + + /** + * Recursively calculate the arabic value of a roman numeral. + * + * @param string[] $roman + */ + private static function calculateArabic(array $roman, int &$sum = 0, int $subtract = 0): int + { + $numeral = array_shift($roman); + if (!isset(self::ROMAN_LOOKUP[$numeral])) { + throw new Exception('Invalid character detected'); + } + + $arabic = self::ROMAN_LOOKUP[$numeral]; + if (count($roman) > 0 && isset(self::ROMAN_LOOKUP[$roman[0]]) && $arabic < self::ROMAN_LOOKUP[$roman[0]]) { + $subtract += $arabic; + } else { + $sum += ($arabic - $subtract); + $subtract = 0; + } + + if (count($roman) > 0) { + self::calculateArabic($roman, $sum, $subtract); + } + + return $sum; + } + + /** + * ARABIC. + * + * Converts a Roman numeral to an Arabic numeral. + * + * Excel Function: + * ARABIC(text) + * + * @param string|string[] $roman Should be a string, or can be an array of strings + * + * @return array|int|string the arabic numeral contrived from the roman numeral + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate(mixed $roman): array|int|string + { + if (is_array($roman)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $roman); + } + + // An empty string should return 0 + $roman = substr(trim(strtoupper((string) $roman)), 0, 255); + if ($roman === '') { + return 0; + } + + // Convert the roman numeral to an arabic number + $negativeNumber = $roman[0] === '-'; + if ($negativeNumber) { + $roman = trim(substr($roman, 1)); + if ($roman === '') { + return ExcelError::NAN(); + } + } + + try { + $arabic = self::calculateArabic(mb_str_split($roman, 1, 'UTF-8')); + } catch (Exception) { + return ExcelError::VALUE(); // Invalid character detected + } + + if ($negativeNumber) { + $arabic *= -1; // The number should be negative + } + + return $arabic; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Base.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Base.php new file mode 100644 index 0000000..e9ef191 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Base.php @@ -0,0 +1,65 @@ +|string the text representation with the given radix (base) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate(mixed $number, mixed $radix, mixed $minLength = null): array|string + { + if (is_array($number) || is_array($radix) || is_array($minLength)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $radix, $minLength); + } + + try { + $number = (float) floor(Helpers::validateNumericNullBool($number)); + $radix = (int) Helpers::validateNumericNullBool($radix); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::calculate($number, $radix, $minLength); + } + + private static function calculate(float $number, int $radix, mixed $minLength): string + { + if ($minLength === null || is_numeric($minLength)) { + if ($number < 0 || $number >= 2 ** 53 || $radix < 2 || $radix > 36) { + return ExcelError::NAN(); // Numeric range constraints + } + + $outcome = strtoupper((string) base_convert("$number", 10, $radix)); + if ($minLength !== null) { + $outcome = str_pad($outcome, (int) $minLength, '0', STR_PAD_LEFT); // String padding + } + + return $outcome; + } + + return ExcelError::VALUE(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php new file mode 100644 index 0000000..3458ca8 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php @@ -0,0 +1,192 @@ +|float $number the number you want the ceiling + * Or can be an array of values + * @param array|float $significance the multiple to which you want to round + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function ceiling($number, $significance = null) + { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + + if ($significance === null) { + self::floorCheck1Arg(); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::argumentsOk((float) $number, (float) $significance); + } + + /** + * CEILING.MATH. + * + * Round a number down to the nearest integer or to the nearest multiple of significance. + * + * Excel Function: + * CEILING.MATH(number[,significance[,mode]]) + * + * @param mixed $number Number to round + * Or can be an array of values + * @param mixed $significance Significance + * Or can be an array of values + * @param array|int $mode direction to round negative numbers + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function math(mixed $number, mixed $significance = null, $mode = 0, bool $checkSigns = false): array|string|float + { + if (is_array($number) || is_array($significance) || is_array($mode)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + $mode = Helpers::validateNumericNullSubstitution($mode, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (empty($significance * $number)) { + return 0.0; + } + if ($checkSigns) { + if (($number > 0 && $significance < 0) || ($number < 0 && $significance > 0)) { + return ExcelError::NAN(); + } + } + if (self::ceilingMathTest((float) $significance, (float) $number, (int) $mode)) { + return floor($number / $significance) * $significance; + } + + return ceil($number / $significance) * $significance; + } + + /** + * CEILING.PRECISE. + * + * Rounds number up, away from zero, to the nearest multiple of significance. + * + * Excel Function: + * CEILING.PRECISE(number[,significance]) + * + * @param mixed $number the number you want to round + * Or can be an array of values + * @param array|float $significance the multiple to which you want to round + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function precise(mixed $number, $significance = 1): array|string|float + { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (!$significance) { + return 0.0; + } + $result = $number / abs($significance); + + return ceil($result) * $significance * (($significance < 0) ? -1 : 1); + } + + /** + * CEILING.ODS, pseudo-function - CEILING as implemented in ODS. + * + * ODS Function (theoretical): + * CEILING.ODS(number[,significance[,mode]]) + * + * @param mixed $number Number to round + * @param mixed $significance Significance + * @param array|int $mode direction to round negative numbers + * + * @return array|float|string Rounded Number, or a string containing an error + */ + public static function mathOds(mixed $number, mixed $significance = null, $mode = 0): array|string|float + { + return self::math($number, $significance, $mode, true); + } + + /** + * Let CEILINGMATH complexity pass Scrutinizer. + */ + private static function ceilingMathTest(float $significance, float $number, int $mode): bool + { + return ($significance < 0) || ($number < 0 && !empty($mode)); + } + + /** + * Avoid Scrutinizer problems concerning complexity. + */ + private static function argumentsOk(float $number, float $significance): float|string + { + if (empty($number * $significance)) { + return 0.0; + } + $signSig = Helpers::returnSign($significance); + $signNum = Helpers::returnSign($number); + if ( + ($signSig === 1 && ($signNum === 1 || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC)) + || ($signSig === -1 && $signNum === -1) + ) { + return ceil($number / $significance) * $significance; + } + + return ExcelError::NAN(); + } + + private static function floorCheck1Arg(): void + { + $compatibility = Functions::getCompatibilityMode(); + if ($compatibility === Functions::COMPATIBILITY_EXCEL) { + throw new Exception('Excel requires 2 arguments for CEILING'); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php new file mode 100644 index 0000000..e38a455 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php @@ -0,0 +1,103 @@ +|float|string Number of combinations, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function withoutRepetition(mixed $numObjs, mixed $numInSet): array|string|float + { + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } + + try { + $numObjs = Helpers::validateNumericNullSubstitution($numObjs, null); + $numInSet = Helpers::validateNumericNullSubstitution($numInSet, null); + Helpers::validateNotNegative($numInSet); + Helpers::validateNotNegative($numObjs - $numInSet); + } catch (Exception $e) { + return $e->getMessage(); + } + + /** @var float */ + $quotient = Factorial::fact($numObjs); + /** @var float */ + $divisor1 = Factorial::fact($numObjs - $numInSet); + /** @var float */ + $divisor2 = Factorial::fact($numInSet); + + return round($quotient / ($divisor1 * $divisor2)); + } + + /** + * COMBINA. + * + * Returns the number of combinations for a given number of items. Use COMBIN to + * determine the total possible number of groups for a given number of items. + * + * Excel Function: + * COMBINA(numObjs,numInSet) + * + * @param mixed $numObjs Number of different objects, or can be an array of numbers + * @param mixed $numInSet Number of objects in each combination, or can be an array of numbers + * + * @return array|float|int|string Number of combinations, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function withRepetition(mixed $numObjs, mixed $numInSet): array|int|string|float + { + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } + + try { + $numObjs = Helpers::validateNumericNullSubstitution($numObjs, null); + $numInSet = Helpers::validateNumericNullSubstitution($numInSet, null); + Helpers::validateNotNegative($numInSet); + Helpers::validateNotNegative($numObjs); + $numObjs = (int) $numObjs; + $numInSet = (int) $numInSet; + // Microsoft documentation says following is true, but Excel + // does not enforce this restriction. + //Helpers::validateNotNegative($numObjs - $numInSet); + if ($numObjs === 0) { + Helpers::validateNotNegative(-$numInSet); + + return 1; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + /** @var float */ + $quotient = Factorial::fact($numObjs + $numInSet - 1); + /** @var float */ + $divisor1 = Factorial::fact($numObjs - 1); + /** @var float */ + $divisor2 = Factorial::fact($numInSet); + + return round($quotient / ($divisor1 * $divisor2)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php new file mode 100644 index 0000000..f029b66 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php @@ -0,0 +1,37 @@ +|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return exp($number); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php new file mode 100644 index 0000000..2e2a921 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php @@ -0,0 +1,128 @@ +|float $factVal Factorial Value, or can be an array of numbers + * + * @return array|float|int|string Factorial, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function fact($factVal): array|string|float|int + { + if (is_array($factVal)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $factVal); + } + + try { + $factVal = Helpers::validateNumericNullBool($factVal); + Helpers::validateNotNegative($factVal); + } catch (Exception $e) { + return $e->getMessage(); + } + + $factLoop = floor($factVal); + if ($factVal > $factLoop) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return Statistical\Distributions\Gamma::gammaValue($factVal + 1); + } + } + + $factorial = 1; + while ($factLoop > 1) { + $factorial *= $factLoop--; + } + + return $factorial; + } + + /** + * FACTDOUBLE. + * + * Returns the double factorial of a number. + * + * Excel Function: + * FACTDOUBLE(factVal) + * + * @param array|float $factVal Factorial Value, or can be an array of numbers + * + * @return array|float|int|string Double Factorial, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function factDouble($factVal): array|string|float|int + { + if (is_array($factVal)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $factVal); + } + + try { + $factVal = Helpers::validateNumericNullSubstitution($factVal, 0); + Helpers::validateNotNegative($factVal); + } catch (Exception $e) { + return $e->getMessage(); + } + + $factLoop = floor($factVal); + $factorial = 1; + while ($factLoop > 1) { + $factorial *= $factLoop; + $factLoop -= 2; + } + + return $factorial; + } + + /** + * MULTINOMIAL. + * + * Returns the ratio of the factorial of a sum of values to the product of factorials. + * + * @param mixed[] $args An array of mixed values for the Data Series + * + * @return float|int|string The result, or a string containing an error + */ + public static function multinomial(...$args): string|int|float + { + $summer = 0; + $divisor = 1; + + try { + // Loop through arguments + foreach (Functions::flattenArray($args) as $argx) { + $arg = Helpers::validateNumericNullSubstitution($argx, null); + Helpers::validateNotNegative($arg); + $arg = (int) $arg; + $summer += $arg; + /** @var float|int */ + $temp = self::fact($arg); + $divisor *= $temp; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $summer = self::fact($summer); + + return is_numeric($summer) ? ($summer / $divisor) : ExcelError::VALUE(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php new file mode 100644 index 0000000..b86fd45 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php @@ -0,0 +1,224 @@ +|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function floor(mixed $number, mixed $significance = null) + { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + + if ($significance === null) { + self::floorCheck1Arg(); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::argumentsOk((float) $number, (float) $significance); + } + + /** + * FLOOR.MATH. + * + * Round a number down to the nearest integer or to the nearest multiple of significance. + * + * Excel Function: + * FLOOR.MATH(number[,significance[,mode]]) + * + * @param mixed $number Number to round + * Or can be an array of values + * @param mixed $significance Significance + * Or can be an array of values + * @param mixed $mode direction to round negative numbers + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function math(mixed $number, mixed $significance = null, mixed $mode = 0, bool $checkSigns = false) + { + if (is_array($number) || is_array($significance) || is_array($mode)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + $mode = Helpers::validateNumericNullSubstitution($mode, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (empty($significance * $number)) { + return 0.0; + } + if ($checkSigns) { + if (($number > 0 && $significance < 0) || ($number < 0 && $significance > 0)) { + return ExcelError::NAN(); + } + } + + return self::argsOk((float) $number, (float) $significance, (int) $mode); + } + + /** + * FLOOR.ODS, pseudo-function - FLOOR as implemented in ODS. + * + * Round a number down to the nearest integer or to the nearest multiple of significance. + * + * ODS Function (theoretical): + * FLOOR.ODS(number[,significance[,mode]]) + * + * @param mixed $number Number to round + * @param mixed $significance Significance + * @param array|int $mode direction to round negative numbers + * + * @return array|float|string Rounded Number, or a string containing an error + */ + public static function mathOds(mixed $number, mixed $significance = null, mixed $mode = 0) + { + return self::math($number, $significance, $mode, true); + } + + /** + * FLOOR.PRECISE. + * + * Rounds number down, toward zero, to the nearest multiple of significance. + * + * Excel Function: + * FLOOR.PRECISE(number[,significance]) + * + * @param array|float $number Number to round + * Or can be an array of values + * @param array|float $significance Significance + * Or can be an array of values + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function precise($number, $significance = 1) + { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, null); + } catch (Exception $e) { + return $e->getMessage(); + } + if (!$significance) { + return 0.0; + } + + return self::argumentsOkPrecise((float) $number, (float) $significance); + } + + /** + * Avoid Scrutinizer problems concerning complexity. + */ + private static function argumentsOkPrecise(float $number, float $significance): string|float + { + if ($significance == 0.0) { + return ExcelError::DIV0(); + } + if ($number == 0.0) { + return 0.0; + } + + return floor($number / abs($significance)) * abs($significance); + } + + /** + * Avoid Scrutinizer complexity problems. + * + * @return float|string Rounded Number, or a string containing an error + */ + private static function argsOk(float $number, float $significance, int $mode): string|float + { + if (!$significance) { + return ExcelError::DIV0(); + } + if (!$number) { + return 0.0; + } + if (self::floorMathTest($number, $significance, $mode)) { + return ceil($number / $significance) * $significance; + } + + return floor($number / $significance) * $significance; + } + + /** + * Let FLOORMATH complexity pass Scrutinizer. + */ + private static function floorMathTest(float $number, float $significance, int $mode): bool + { + return Helpers::returnSign($significance) == -1 || (Helpers::returnSign($number) == -1 && !empty($mode)); + } + + /** + * Avoid Scrutinizer problems concerning complexity. + */ + private static function argumentsOk(float $number, float $significance): string|float + { + if ($significance == 0.0) { + return ExcelError::DIV0(); + } + if ($number == 0.0) { + return 0.0; + } + $signSig = Helpers::returnSign($significance); + $signNum = Helpers::returnSign($number); + if ( + ($signSig === 1 && ($signNum === 1 || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC)) + || ($signNum === -1 && $signSig === -1) + ) { + return floor($number / $significance) * $significance; + } + + return ExcelError::NAN(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php new file mode 100644 index 0000000..f2aedb6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php @@ -0,0 +1,65 @@ +getMessage(); + } + + if (count($arrayArgs) <= 0) { + return ExcelError::VALUE(); + } + $gcd = (int) array_pop($arrayArgs); + do { + $gcd = self::evaluateGCD($gcd, (int) array_pop($arrayArgs)); + } while (!empty($arrayArgs)); + + return $gcd; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php new file mode 100644 index 0000000..57e05b1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -0,0 +1,111 @@ += 0. + */ + public static function validateNotNegative(float|int $number, ?string $except = null): void + { + if ($number >= 0) { + return; + } + + throw new Exception($except ?? ExcelError::NAN()); + } + + /** + * Confirm number > 0. + */ + public static function validatePositive(float|int $number, ?string $except = null): void + { + if ($number > 0) { + return; + } + + throw new Exception($except ?? ExcelError::NAN()); + } + + /** + * Confirm number != 0. + */ + public static function validateNotZero(float|int $number): void + { + if ($number) { + return; + } + + throw new Exception(ExcelError::DIV0()); + } + + public static function returnSign(float $number): int + { + return $number ? (($number > 0) ? 1 : -1) : 0; + } + + public static function getEven(float $number): float + { + $significance = 2 * self::returnSign($number); + + return $significance ? (ceil($number / $significance) * $significance) : 0; + } + + /** + * Return NAN or value depending on argument. + */ + public static function numberOrNan(float $result): float|string + { + return is_nan($result) ? ExcelError::NAN() : $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php new file mode 100644 index 0000000..f7d6da4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php @@ -0,0 +1,40 @@ +|float $number Number to cast to an integer, or can be an array of numbers + * + * @return array|int|string Integer value, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate($number): array|string|int + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (int) floor($number); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php new file mode 100644 index 0000000..88a5222 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php @@ -0,0 +1,119 @@ + 1; --$i) { + if (($value % $i) == 0) { + $factorArray = array_merge($factorArray, self::factors($value / $i)); + $factorArray = array_merge($factorArray, self::factors($i)); + if ($i <= sqrt($value)) { + break; + } + } + } + if (!empty($factorArray)) { + rsort($factorArray); + /** @var int[] $factorArray */ + + return $factorArray; + } + + return [(int) $value]; + } + + /** + * LCM. + * + * Returns the lowest common multiplier of a series of numbers + * The least common multiple is the smallest positive integer that is a multiple + * of all integer arguments number1, number2, and so on. Use LCM to add fractions + * with different denominators. + * + * Excel Function: + * LCM(number1[,number2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return int|string Lowest Common Multiplier, or a string containing an error + */ + public static function evaluate(mixed ...$args): int|string + { + try { + $arrayArgs = []; + $anyZeros = 0; + $anyNonNulls = 0; + foreach (Functions::flattenArray($args) as $value1) { + $anyNonNulls += (int) ($value1 !== null); + $value = Helpers::validateNumericNullSubstitution($value1, 1); + Helpers::validateNotNegative($value); + $arrayArgs[] = (int) $value; + $anyZeros += (int) !((bool) $value); + } + self::testNonNulls($anyNonNulls); + if ($anyZeros) { + return 0; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $returnValue = 1; + $allPoweredFactors = []; + // Loop through arguments + foreach ($arrayArgs as $value) { + $myFactors = self::factors(floor($value)); + $myCountedFactors = array_count_values($myFactors); + $myPoweredFactors = []; + foreach ($myCountedFactors as $myCountedFactor => $myCountedPower) { + $myPoweredFactors[$myCountedFactor] = $myCountedFactor ** $myCountedPower; + } + self::processPoweredFactors($allPoweredFactors, $myPoweredFactors); + } + foreach ($allPoweredFactors as $allPoweredFactor) { + /** @var scalar $allPoweredFactor */ + $returnValue *= (int) $allPoweredFactor; + } + + return $returnValue; + } + + /** + * @param mixed[] $allPoweredFactors + * @param mixed[] $myPoweredFactors + */ + private static function processPoweredFactors(array &$allPoweredFactors, array &$myPoweredFactors): void + { + foreach ($myPoweredFactors as $myPoweredValue => $myPoweredFactor) { + if (isset($allPoweredFactors[$myPoweredValue])) { + if ($allPoweredFactors[$myPoweredValue] < $myPoweredFactor) { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } else { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } + } + + private static function testNonNulls(int $anyNonNulls): void + { + if (!$anyNonNulls) { + throw new Exception(ExcelError::VALUE()); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php new file mode 100644 index 0000000..f2ccdf6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php @@ -0,0 +1,102 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function withBase(mixed $number, mixed $base = 10): array|string|float + { + if (is_array($number) || is_array($base)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $base); + } + + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + $base = Helpers::validateNumericNullBool($base); + Helpers::validatePositive($base); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log($number, $base); + } + + /** + * LOG10. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * Or can be an array of values + * + * @return array|float|string Rounded number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function base10(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log10($number); + } + + /** + * LN. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * Or can be an array of values + * + * @return array|float|string Rounded number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function natural(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log($number); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php new file mode 100644 index 0000000..df61e08 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php @@ -0,0 +1,179 @@ +|string The resulting array, or a string containing an error + */ + public static function sequence(mixed $rows = 1, mixed $columns = 1, mixed $start = 1, mixed $step = 1): string|array + { + try { + $rows = (int) Helpers::validateNumericNullSubstitution($rows, 1); + Helpers::validatePositive($rows); + $columns = (int) Helpers::validateNumericNullSubstitution($columns, 1); + Helpers::validatePositive($columns); + $start = Helpers::validateNumericNullSubstitution($start, 1); + $step = Helpers::validateNumericNullSubstitution($step, 1); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($step === 0) { + return array_chunk( + array_fill(0, $rows * $columns, $start), + max($columns, 1) + ); + } + + return array_chunk( + range($start, $start + (($rows * $columns - 1) * $step), $step), + max($columns, 1) + ); + } + + /** + * MDETERM. + * + * Returns the matrix determinant of an array. + * + * Excel Function: + * MDETERM(array) + * + * @param mixed $matrixValues A matrix of values + * + * @return float|string The result, or a string containing an error + */ + public static function determinant(mixed $matrixValues) + { + try { + $matrix = self::getMatrix($matrixValues); + + return $matrix->determinant(); + } catch (MatrixException) { + return ExcelError::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MINVERSE. + * + * Returns the inverse matrix for the matrix stored in an array. + * + * Excel Function: + * MINVERSE(array) + * + * @param mixed $matrixValues A matrix of values + * + * @return array|string The result, or a string containing an error + */ + public static function inverse(mixed $matrixValues): array|string + { + try { + $matrix = self::getMatrix($matrixValues); + + return $matrix->inverse()->toArray(); + } catch (MatrixDiv0Exception) { + return ExcelError::NAN(); + } catch (MatrixException) { + return ExcelError::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MMULT. + * + * @param mixed $matrixData1 A matrix of values + * @param mixed $matrixData2 A matrix of values + * + * @return array|string The result, or a string containing an error + */ + public static function multiply(mixed $matrixData1, mixed $matrixData2): array|string + { + try { + $matrixA = self::getMatrix($matrixData1); + $matrixB = self::getMatrix($matrixData2); + + return $matrixA->multiply($matrixB)->toArray(); + } catch (MatrixException) { + return ExcelError::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MUnit. + * + * @param mixed $dimension Number of rows and columns + * + * @return array|string The result, or a string containing an error + */ + public static function identity(mixed $dimension) + { + try { + $dimension = (int) Helpers::validateNumericNullBool($dimension); + Helpers::validatePositive($dimension, ExcelError::VALUE()); + $matrix = Builder::createIdentityMatrix($dimension, 0)->toArray(); + + return $matrix; + } catch (Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php new file mode 100644 index 0000000..40b6dcd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -0,0 +1,155 @@ +|float|string Remainder, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function mod(mixed $dividend, mixed $divisor): array|string|float + { + if (is_array($dividend) || is_array($divisor)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dividend, $divisor); + } + + try { + $dividend = Helpers::validateNumericNullBool($dividend); + $divisor = Helpers::validateNumericNullBool($divisor); + Helpers::validateNotZero($divisor); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($dividend < 0.0) && ($divisor > 0.0)) { + return $divisor - fmod(abs($dividend), $divisor); + } + if (($dividend > 0.0) && ($divisor < 0.0)) { + return $divisor + fmod($dividend, abs($divisor)); + } + + return fmod($dividend, $divisor); + } + + /** + * POWER. + * + * Computes x raised to the power y. + * + * @param null|array|bool|float|int|string $x Or can be an array of values + * @param null|array|bool|float|int|string $y Or can be an array of values + * + * @return array|float|int|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function power(null|array|bool|float|int|string $x, null|array|bool|float|int|string $y): array|float|int|string + { + if (is_array($x) || is_array($y)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $y); + } + + try { + $x = Helpers::validateNumericNullBool($x); + $y = Helpers::validateNumericNullBool($y); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if (!$x && !$y) { + return ExcelError::NAN(); + } + if (!$x && $y < 0.0) { + return ExcelError::DIV0(); + } + + // Return + $result = $x ** $y; + + return Helpers::numberOrNan($result); + } + + /** + * PRODUCT. + * + * PRODUCT returns the product of all the values and cells referenced in the argument list. + * + * Excel Function: + * PRODUCT(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + */ + public static function product(mixed ...$args): string|float + { + $args = array_filter( + Functions::flattenArray($args), + fn ($value): bool => $value !== null + ); + + // Return value + $returnValue = (count($args) === 0) ? 0.0 : 1.0; + + // Loop through arguments + foreach ($args as $arg) { + // Is it a numeric value? + if (is_numeric($arg)) { + $returnValue *= $arg; + } else { + return ExcelError::throwError($arg); + } + } + + return (float) $returnValue; + } + + /** + * QUOTIENT. + * + * QUOTIENT function returns the integer portion of a division. Numerator is the divided number + * and denominator is the divisor. + * + * Excel Function: + * QUOTIENT(value1,value2) + * + * @param mixed $numerator Expect float|int + * Or can be an array of values + * @param mixed $denominator Expect float|int + * Or can be an array of values + * + * @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function quotient(mixed $numerator, mixed $denominator): array|string|int + { + if (is_array($numerator) || is_array($denominator)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numerator, $denominator); + } + + try { + $numerator = Helpers::validateNumericNullSubstitution($numerator, 0); + $denominator = Helpers::validateNumericNullSubstitution($denominator, 0); + Helpers::validateNotZero($denominator); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (int) ($numerator / $denominator); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Random.php new file mode 100644 index 0000000..4db946a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -0,0 +1,97 @@ +|int|string Random number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function randBetween(mixed $min, mixed $max): array|string|int + { + if (is_array($min) || is_array($max)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $min, $max); + } + + try { + $min = (int) Helpers::validateNumericNullBool($min); + $max = (int) Helpers::validateNumericNullBool($max); + Helpers::validateNotNegative($max - $min); + } catch (Exception $e) { + return $e->getMessage(); + } + + return mt_rand($min, $max); + } + + /** + * RANDARRAY. + * + * Generates a list of sequential numbers in an array. + * + * Excel Function: + * RANDARRAY([rows],[columns],[start],[step]) + * + * @param mixed $rows the number of rows to return, defaults to 1 + * @param mixed $columns the number of columns to return, defaults to 1 + * @param mixed $min the minimum number to be returned, defaults to 0 + * @param mixed $max the maximum number to be returned, defaults to 1 + * @param bool $wholeNumber the type of numbers to return: + * False - Decimal numbers to 15 decimal places. (default) + * True - Whole (integer) numbers + * + * @return array|string The resulting array, or a string containing an error + */ + public static function randArray(mixed $rows = 1, mixed $columns = 1, mixed $min = 0, mixed $max = 1, bool $wholeNumber = false): string|array + { + try { + $rows = (int) Helpers::validateNumericNullSubstitution($rows, 1); + Helpers::validatePositive($rows); + $columns = (int) Helpers::validateNumericNullSubstitution($columns, 1); + Helpers::validatePositive($columns); + $min = Helpers::validateNumericNullSubstitution($min, 1); + $max = Helpers::validateNumericNullSubstitution($max, 1); + + if ($max <= $min) { + return ExcelError::VALUE(); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return array_chunk( + array_map( + fn (): int|float => $wholeNumber + ? mt_rand((int) $min, (int) $max) + : (mt_rand() / mt_getrandmax()) * ($max - $min) + $min, + array_fill(0, $rows * $columns, $min) + ), + max($columns, 1) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php new file mode 100644 index 0000000..efb9dc0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php @@ -0,0 +1,846 @@ + ['VL'], + 46 => ['VLI'], + 47 => ['VLII'], + 48 => ['VLIII'], + 49 => ['VLIV', 'IL'], + 95 => ['VC'], + 96 => ['VCI'], + 97 => ['VCII'], + 98 => ['VCIII'], + 99 => ['VCIV', 'IC'], + 145 => ['CVL'], + 146 => ['CVLI'], + 147 => ['CVLII'], + 148 => ['CVLIII'], + 149 => ['CVLIV', 'CIL'], + 195 => ['CVC'], + 196 => ['CVCI'], + 197 => ['CVCII'], + 198 => ['CVCIII'], + 199 => ['CVCIV', 'CIC'], + 245 => ['CCVL'], + 246 => ['CCVLI'], + 247 => ['CCVLII'], + 248 => ['CCVLIII'], + 249 => ['CCVLIV', 'CCIL'], + 295 => ['CCVC'], + 296 => ['CCVCI'], + 297 => ['CCVCII'], + 298 => ['CCVCIII'], + 299 => ['CCVCIV', 'CCIC'], + 345 => ['CCCVL'], + 346 => ['CCCVLI'], + 347 => ['CCCVLII'], + 348 => ['CCCVLIII'], + 349 => ['CCCVLIV', 'CCCIL'], + 395 => ['CCCVC'], + 396 => ['CCCVCI'], + 397 => ['CCCVCII'], + 398 => ['CCCVCIII'], + 399 => ['CCCVCIV', 'CCCIC'], + 445 => ['CDVL'], + 446 => ['CDVLI'], + 447 => ['CDVLII'], + 448 => ['CDVLIII'], + 449 => ['CDVLIV', 'CDIL'], + 450 => ['LD'], + 451 => ['LDI'], + 452 => ['LDII'], + 453 => ['LDIII'], + 454 => ['LDIV'], + 455 => ['LDV'], + 456 => ['LDVI'], + 457 => ['LDVII'], + 458 => ['LDVIII'], + 459 => ['LDIX'], + 460 => ['LDX'], + 461 => ['LDXI'], + 462 => ['LDXII'], + 463 => ['LDXIII'], + 464 => ['LDXIV'], + 465 => ['LDXV'], + 466 => ['LDXVI'], + 467 => ['LDXVII'], + 468 => ['LDXVIII'], + 469 => ['LDXIX'], + 470 => ['LDXX'], + 471 => ['LDXXI'], + 472 => ['LDXXII'], + 473 => ['LDXXIII'], + 474 => ['LDXXIV'], + 475 => ['LDXXV'], + 476 => ['LDXXVI'], + 477 => ['LDXXVII'], + 478 => ['LDXXVIII'], + 479 => ['LDXXIX'], + 480 => ['LDXXX'], + 481 => ['LDXXXI'], + 482 => ['LDXXXII'], + 483 => ['LDXXXIII'], + 484 => ['LDXXXIV'], + 485 => ['LDXXXV'], + 486 => ['LDXXXVI'], + 487 => ['LDXXXVII'], + 488 => ['LDXXXVIII'], + 489 => ['LDXXXIX'], + 490 => ['LDXL', 'XD'], + 491 => ['LDXLI', 'XDI'], + 492 => ['LDXLII', 'XDII'], + 493 => ['LDXLIII', 'XDIII'], + 494 => ['LDXLIV', 'XDIV'], + 495 => ['LDVL', 'XDV', 'VD'], + 496 => ['LDVLI', 'XDVI', 'VDI'], + 497 => ['LDVLII', 'XDVII', 'VDII'], + 498 => ['LDVLIII', 'XDVIII', 'VDIII'], + 499 => ['LDVLIV', 'XDIX', 'VDIV', 'ID'], + 545 => ['DVL'], + 546 => ['DVLI'], + 547 => ['DVLII'], + 548 => ['DVLIII'], + 549 => ['DVLIV', 'DIL'], + 595 => ['DVC'], + 596 => ['DVCI'], + 597 => ['DVCII'], + 598 => ['DVCIII'], + 599 => ['DVCIV', 'DIC'], + 645 => ['DCVL'], + 646 => ['DCVLI'], + 647 => ['DCVLII'], + 648 => ['DCVLIII'], + 649 => ['DCVLIV', 'DCIL'], + 695 => ['DCVC'], + 696 => ['DCVCI'], + 697 => ['DCVCII'], + 698 => ['DCVCIII'], + 699 => ['DCVCIV', 'DCIC'], + 745 => ['DCCVL'], + 746 => ['DCCVLI'], + 747 => ['DCCVLII'], + 748 => ['DCCVLIII'], + 749 => ['DCCVLIV', 'DCCIL'], + 795 => ['DCCVC'], + 796 => ['DCCVCI'], + 797 => ['DCCVCII'], + 798 => ['DCCVCIII'], + 799 => ['DCCVCIV', 'DCCIC'], + 845 => ['DCCCVL'], + 846 => ['DCCCVLI'], + 847 => ['DCCCVLII'], + 848 => ['DCCCVLIII'], + 849 => ['DCCCVLIV', 'DCCCIL'], + 895 => ['DCCCVC'], + 896 => ['DCCCVCI'], + 897 => ['DCCCVCII'], + 898 => ['DCCCVCIII'], + 899 => ['DCCCVCIV', 'DCCCIC'], + 945 => ['CMVL'], + 946 => ['CMVLI'], + 947 => ['CMVLII'], + 948 => ['CMVLIII'], + 949 => ['CMVLIV', 'CMIL'], + 950 => ['LM'], + 951 => ['LMI'], + 952 => ['LMII'], + 953 => ['LMIII'], + 954 => ['LMIV'], + 955 => ['LMV'], + 956 => ['LMVI'], + 957 => ['LMVII'], + 958 => ['LMVIII'], + 959 => ['LMIX'], + 960 => ['LMX'], + 961 => ['LMXI'], + 962 => ['LMXII'], + 963 => ['LMXIII'], + 964 => ['LMXIV'], + 965 => ['LMXV'], + 966 => ['LMXVI'], + 967 => ['LMXVII'], + 968 => ['LMXVIII'], + 969 => ['LMXIX'], + 970 => ['LMXX'], + 971 => ['LMXXI'], + 972 => ['LMXXII'], + 973 => ['LMXXIII'], + 974 => ['LMXXIV'], + 975 => ['LMXXV'], + 976 => ['LMXXVI'], + 977 => ['LMXXVII'], + 978 => ['LMXXVIII'], + 979 => ['LMXXIX'], + 980 => ['LMXXX'], + 981 => ['LMXXXI'], + 982 => ['LMXXXII'], + 983 => ['LMXXXIII'], + 984 => ['LMXXXIV'], + 985 => ['LMXXXV'], + 986 => ['LMXXXVI'], + 987 => ['LMXXXVII'], + 988 => ['LMXXXVIII'], + 989 => ['LMXXXIX'], + 990 => ['LMXL', 'XM'], + 991 => ['LMXLI', 'XMI'], + 992 => ['LMXLII', 'XMII'], + 993 => ['LMXLIII', 'XMIII'], + 994 => ['LMXLIV', 'XMIV'], + 995 => ['LMVL', 'XMV', 'VM'], + 996 => ['LMVLI', 'XMVI', 'VMI'], + 997 => ['LMVLII', 'XMVII', 'VMII'], + 998 => ['LMVLIII', 'XMVIII', 'VMIII'], + 999 => ['LMVLIV', 'XMIX', 'VMIV', 'IM'], + 1045 => ['MVL'], + 1046 => ['MVLI'], + 1047 => ['MVLII'], + 1048 => ['MVLIII'], + 1049 => ['MVLIV', 'MIL'], + 1095 => ['MVC'], + 1096 => ['MVCI'], + 1097 => ['MVCII'], + 1098 => ['MVCIII'], + 1099 => ['MVCIV', 'MIC'], + 1145 => ['MCVL'], + 1146 => ['MCVLI'], + 1147 => ['MCVLII'], + 1148 => ['MCVLIII'], + 1149 => ['MCVLIV', 'MCIL'], + 1195 => ['MCVC'], + 1196 => ['MCVCI'], + 1197 => ['MCVCII'], + 1198 => ['MCVCIII'], + 1199 => ['MCVCIV', 'MCIC'], + 1245 => ['MCCVL'], + 1246 => ['MCCVLI'], + 1247 => ['MCCVLII'], + 1248 => ['MCCVLIII'], + 1249 => ['MCCVLIV', 'MCCIL'], + 1295 => ['MCCVC'], + 1296 => ['MCCVCI'], + 1297 => ['MCCVCII'], + 1298 => ['MCCVCIII'], + 1299 => ['MCCVCIV', 'MCCIC'], + 1345 => ['MCCCVL'], + 1346 => ['MCCCVLI'], + 1347 => ['MCCCVLII'], + 1348 => ['MCCCVLIII'], + 1349 => ['MCCCVLIV', 'MCCCIL'], + 1395 => ['MCCCVC'], + 1396 => ['MCCCVCI'], + 1397 => ['MCCCVCII'], + 1398 => ['MCCCVCIII'], + 1399 => ['MCCCVCIV', 'MCCCIC'], + 1445 => ['MCDVL'], + 1446 => ['MCDVLI'], + 1447 => ['MCDVLII'], + 1448 => ['MCDVLIII'], + 1449 => ['MCDVLIV', 'MCDIL'], + 1450 => ['MLD'], + 1451 => ['MLDI'], + 1452 => ['MLDII'], + 1453 => ['MLDIII'], + 1454 => ['MLDIV'], + 1455 => ['MLDV'], + 1456 => ['MLDVI'], + 1457 => ['MLDVII'], + 1458 => ['MLDVIII'], + 1459 => ['MLDIX'], + 1460 => ['MLDX'], + 1461 => ['MLDXI'], + 1462 => ['MLDXII'], + 1463 => ['MLDXIII'], + 1464 => ['MLDXIV'], + 1465 => ['MLDXV'], + 1466 => ['MLDXVI'], + 1467 => ['MLDXVII'], + 1468 => ['MLDXVIII'], + 1469 => ['MLDXIX'], + 1470 => ['MLDXX'], + 1471 => ['MLDXXI'], + 1472 => ['MLDXXII'], + 1473 => ['MLDXXIII'], + 1474 => ['MLDXXIV'], + 1475 => ['MLDXXV'], + 1476 => ['MLDXXVI'], + 1477 => ['MLDXXVII'], + 1478 => ['MLDXXVIII'], + 1479 => ['MLDXXIX'], + 1480 => ['MLDXXX'], + 1481 => ['MLDXXXI'], + 1482 => ['MLDXXXII'], + 1483 => ['MLDXXXIII'], + 1484 => ['MLDXXXIV'], + 1485 => ['MLDXXXV'], + 1486 => ['MLDXXXVI'], + 1487 => ['MLDXXXVII'], + 1488 => ['MLDXXXVIII'], + 1489 => ['MLDXXXIX'], + 1490 => ['MLDXL', 'MXD'], + 1491 => ['MLDXLI', 'MXDI'], + 1492 => ['MLDXLII', 'MXDII'], + 1493 => ['MLDXLIII', 'MXDIII'], + 1494 => ['MLDXLIV', 'MXDIV'], + 1495 => ['MLDVL', 'MXDV', 'MVD'], + 1496 => ['MLDVLI', 'MXDVI', 'MVDI'], + 1497 => ['MLDVLII', 'MXDVII', 'MVDII'], + 1498 => ['MLDVLIII', 'MXDVIII', 'MVDIII'], + 1499 => ['MLDVLIV', 'MXDIX', 'MVDIV', 'MID'], + 1545 => ['MDVL'], + 1546 => ['MDVLI'], + 1547 => ['MDVLII'], + 1548 => ['MDVLIII'], + 1549 => ['MDVLIV', 'MDIL'], + 1595 => ['MDVC'], + 1596 => ['MDVCI'], + 1597 => ['MDVCII'], + 1598 => ['MDVCIII'], + 1599 => ['MDVCIV', 'MDIC'], + 1645 => ['MDCVL'], + 1646 => ['MDCVLI'], + 1647 => ['MDCVLII'], + 1648 => ['MDCVLIII'], + 1649 => ['MDCVLIV', 'MDCIL'], + 1695 => ['MDCVC'], + 1696 => ['MDCVCI'], + 1697 => ['MDCVCII'], + 1698 => ['MDCVCIII'], + 1699 => ['MDCVCIV', 'MDCIC'], + 1745 => ['MDCCVL'], + 1746 => ['MDCCVLI'], + 1747 => ['MDCCVLII'], + 1748 => ['MDCCVLIII'], + 1749 => ['MDCCVLIV', 'MDCCIL'], + 1795 => ['MDCCVC'], + 1796 => ['MDCCVCI'], + 1797 => ['MDCCVCII'], + 1798 => ['MDCCVCIII'], + 1799 => ['MDCCVCIV', 'MDCCIC'], + 1845 => ['MDCCCVL'], + 1846 => ['MDCCCVLI'], + 1847 => ['MDCCCVLII'], + 1848 => ['MDCCCVLIII'], + 1849 => ['MDCCCVLIV', 'MDCCCIL'], + 1895 => ['MDCCCVC'], + 1896 => ['MDCCCVCI'], + 1897 => ['MDCCCVCII'], + 1898 => ['MDCCCVCIII'], + 1899 => ['MDCCCVCIV', 'MDCCCIC'], + 1945 => ['MCMVL'], + 1946 => ['MCMVLI'], + 1947 => ['MCMVLII'], + 1948 => ['MCMVLIII'], + 1949 => ['MCMVLIV', 'MCMIL'], + 1950 => ['MLM'], + 1951 => ['MLMI'], + 1952 => ['MLMII'], + 1953 => ['MLMIII'], + 1954 => ['MLMIV'], + 1955 => ['MLMV'], + 1956 => ['MLMVI'], + 1957 => ['MLMVII'], + 1958 => ['MLMVIII'], + 1959 => ['MLMIX'], + 1960 => ['MLMX'], + 1961 => ['MLMXI'], + 1962 => ['MLMXII'], + 1963 => ['MLMXIII'], + 1964 => ['MLMXIV'], + 1965 => ['MLMXV'], + 1966 => ['MLMXVI'], + 1967 => ['MLMXVII'], + 1968 => ['MLMXVIII'], + 1969 => ['MLMXIX'], + 1970 => ['MLMXX'], + 1971 => ['MLMXXI'], + 1972 => ['MLMXXII'], + 1973 => ['MLMXXIII'], + 1974 => ['MLMXXIV'], + 1975 => ['MLMXXV'], + 1976 => ['MLMXXVI'], + 1977 => ['MLMXXVII'], + 1978 => ['MLMXXVIII'], + 1979 => ['MLMXXIX'], + 1980 => ['MLMXXX'], + 1981 => ['MLMXXXI'], + 1982 => ['MLMXXXII'], + 1983 => ['MLMXXXIII'], + 1984 => ['MLMXXXIV'], + 1985 => ['MLMXXXV'], + 1986 => ['MLMXXXVI'], + 1987 => ['MLMXXXVII'], + 1988 => ['MLMXXXVIII'], + 1989 => ['MLMXXXIX'], + 1990 => ['MLMXL', 'MXM'], + 1991 => ['MLMXLI', 'MXMI'], + 1992 => ['MLMXLII', 'MXMII'], + 1993 => ['MLMXLIII', 'MXMIII'], + 1994 => ['MLMXLIV', 'MXMIV'], + 1995 => ['MLMVL', 'MXMV', 'MVM'], + 1996 => ['MLMVLI', 'MXMVI', 'MVMI'], + 1997 => ['MLMVLII', 'MXMVII', 'MVMII'], + 1998 => ['MLMVLIII', 'MXMVIII', 'MVMIII'], + 1999 => ['MLMVLIV', 'MXMIX', 'MVMIV', 'MIM'], + 2045 => ['MMVL'], + 2046 => ['MMVLI'], + 2047 => ['MMVLII'], + 2048 => ['MMVLIII'], + 2049 => ['MMVLIV', 'MMIL'], + 2095 => ['MMVC'], + 2096 => ['MMVCI'], + 2097 => ['MMVCII'], + 2098 => ['MMVCIII'], + 2099 => ['MMVCIV', 'MMIC'], + 2145 => ['MMCVL'], + 2146 => ['MMCVLI'], + 2147 => ['MMCVLII'], + 2148 => ['MMCVLIII'], + 2149 => ['MMCVLIV', 'MMCIL'], + 2195 => ['MMCVC'], + 2196 => ['MMCVCI'], + 2197 => ['MMCVCII'], + 2198 => ['MMCVCIII'], + 2199 => ['MMCVCIV', 'MMCIC'], + 2245 => ['MMCCVL'], + 2246 => ['MMCCVLI'], + 2247 => ['MMCCVLII'], + 2248 => ['MMCCVLIII'], + 2249 => ['MMCCVLIV', 'MMCCIL'], + 2295 => ['MMCCVC'], + 2296 => ['MMCCVCI'], + 2297 => ['MMCCVCII'], + 2298 => ['MMCCVCIII'], + 2299 => ['MMCCVCIV', 'MMCCIC'], + 2345 => ['MMCCCVL'], + 2346 => ['MMCCCVLI'], + 2347 => ['MMCCCVLII'], + 2348 => ['MMCCCVLIII'], + 2349 => ['MMCCCVLIV', 'MMCCCIL'], + 2395 => ['MMCCCVC'], + 2396 => ['MMCCCVCI'], + 2397 => ['MMCCCVCII'], + 2398 => ['MMCCCVCIII'], + 2399 => ['MMCCCVCIV', 'MMCCCIC'], + 2445 => ['MMCDVL'], + 2446 => ['MMCDVLI'], + 2447 => ['MMCDVLII'], + 2448 => ['MMCDVLIII'], + 2449 => ['MMCDVLIV', 'MMCDIL'], + 2450 => ['MMLD'], + 2451 => ['MMLDI'], + 2452 => ['MMLDII'], + 2453 => ['MMLDIII'], + 2454 => ['MMLDIV'], + 2455 => ['MMLDV'], + 2456 => ['MMLDVI'], + 2457 => ['MMLDVII'], + 2458 => ['MMLDVIII'], + 2459 => ['MMLDIX'], + 2460 => ['MMLDX'], + 2461 => ['MMLDXI'], + 2462 => ['MMLDXII'], + 2463 => ['MMLDXIII'], + 2464 => ['MMLDXIV'], + 2465 => ['MMLDXV'], + 2466 => ['MMLDXVI'], + 2467 => ['MMLDXVII'], + 2468 => ['MMLDXVIII'], + 2469 => ['MMLDXIX'], + 2470 => ['MMLDXX'], + 2471 => ['MMLDXXI'], + 2472 => ['MMLDXXII'], + 2473 => ['MMLDXXIII'], + 2474 => ['MMLDXXIV'], + 2475 => ['MMLDXXV'], + 2476 => ['MMLDXXVI'], + 2477 => ['MMLDXXVII'], + 2478 => ['MMLDXXVIII'], + 2479 => ['MMLDXXIX'], + 2480 => ['MMLDXXX'], + 2481 => ['MMLDXXXI'], + 2482 => ['MMLDXXXII'], + 2483 => ['MMLDXXXIII'], + 2484 => ['MMLDXXXIV'], + 2485 => ['MMLDXXXV'], + 2486 => ['MMLDXXXVI'], + 2487 => ['MMLDXXXVII'], + 2488 => ['MMLDXXXVIII'], + 2489 => ['MMLDXXXIX'], + 2490 => ['MMLDXL', 'MMXD'], + 2491 => ['MMLDXLI', 'MMXDI'], + 2492 => ['MMLDXLII', 'MMXDII'], + 2493 => ['MMLDXLIII', 'MMXDIII'], + 2494 => ['MMLDXLIV', 'MMXDIV'], + 2495 => ['MMLDVL', 'MMXDV', 'MMVD'], + 2496 => ['MMLDVLI', 'MMXDVI', 'MMVDI'], + 2497 => ['MMLDVLII', 'MMXDVII', 'MMVDII'], + 2498 => ['MMLDVLIII', 'MMXDVIII', 'MMVDIII'], + 2499 => ['MMLDVLIV', 'MMXDIX', 'MMVDIV', 'MMID'], + 2545 => ['MMDVL'], + 2546 => ['MMDVLI'], + 2547 => ['MMDVLII'], + 2548 => ['MMDVLIII'], + 2549 => ['MMDVLIV', 'MMDIL'], + 2595 => ['MMDVC'], + 2596 => ['MMDVCI'], + 2597 => ['MMDVCII'], + 2598 => ['MMDVCIII'], + 2599 => ['MMDVCIV', 'MMDIC'], + 2645 => ['MMDCVL'], + 2646 => ['MMDCVLI'], + 2647 => ['MMDCVLII'], + 2648 => ['MMDCVLIII'], + 2649 => ['MMDCVLIV', 'MMDCIL'], + 2695 => ['MMDCVC'], + 2696 => ['MMDCVCI'], + 2697 => ['MMDCVCII'], + 2698 => ['MMDCVCIII'], + 2699 => ['MMDCVCIV', 'MMDCIC'], + 2745 => ['MMDCCVL'], + 2746 => ['MMDCCVLI'], + 2747 => ['MMDCCVLII'], + 2748 => ['MMDCCVLIII'], + 2749 => ['MMDCCVLIV', 'MMDCCIL'], + 2795 => ['MMDCCVC'], + 2796 => ['MMDCCVCI'], + 2797 => ['MMDCCVCII'], + 2798 => ['MMDCCVCIII'], + 2799 => ['MMDCCVCIV', 'MMDCCIC'], + 2845 => ['MMDCCCVL'], + 2846 => ['MMDCCCVLI'], + 2847 => ['MMDCCCVLII'], + 2848 => ['MMDCCCVLIII'], + 2849 => ['MMDCCCVLIV', 'MMDCCCIL'], + 2895 => ['MMDCCCVC'], + 2896 => ['MMDCCCVCI'], + 2897 => ['MMDCCCVCII'], + 2898 => ['MMDCCCVCIII'], + 2899 => ['MMDCCCVCIV', 'MMDCCCIC'], + 2945 => ['MMCMVL'], + 2946 => ['MMCMVLI'], + 2947 => ['MMCMVLII'], + 2948 => ['MMCMVLIII'], + 2949 => ['MMCMVLIV', 'MMCMIL'], + 2950 => ['MMLM'], + 2951 => ['MMLMI'], + 2952 => ['MMLMII'], + 2953 => ['MMLMIII'], + 2954 => ['MMLMIV'], + 2955 => ['MMLMV'], + 2956 => ['MMLMVI'], + 2957 => ['MMLMVII'], + 2958 => ['MMLMVIII'], + 2959 => ['MMLMIX'], + 2960 => ['MMLMX'], + 2961 => ['MMLMXI'], + 2962 => ['MMLMXII'], + 2963 => ['MMLMXIII'], + 2964 => ['MMLMXIV'], + 2965 => ['MMLMXV'], + 2966 => ['MMLMXVI'], + 2967 => ['MMLMXVII'], + 2968 => ['MMLMXVIII'], + 2969 => ['MMLMXIX'], + 2970 => ['MMLMXX'], + 2971 => ['MMLMXXI'], + 2972 => ['MMLMXXII'], + 2973 => ['MMLMXXIII'], + 2974 => ['MMLMXXIV'], + 2975 => ['MMLMXXV'], + 2976 => ['MMLMXXVI'], + 2977 => ['MMLMXXVII'], + 2978 => ['MMLMXXVIII'], + 2979 => ['MMLMXXIX'], + 2980 => ['MMLMXXX'], + 2981 => ['MMLMXXXI'], + 2982 => ['MMLMXXXII'], + 2983 => ['MMLMXXXIII'], + 2984 => ['MMLMXXXIV'], + 2985 => ['MMLMXXXV'], + 2986 => ['MMLMXXXVI'], + 2987 => ['MMLMXXXVII'], + 2988 => ['MMLMXXXVIII'], + 2989 => ['MMLMXXXIX'], + 2990 => ['MMLMXL', 'MMXM'], + 2991 => ['MMLMXLI', 'MMXMI'], + 2992 => ['MMLMXLII', 'MMXMII'], + 2993 => ['MMLMXLIII', 'MMXMIII'], + 2994 => ['MMLMXLIV', 'MMXMIV'], + 2995 => ['MMLMVL', 'MMXMV', 'MMVM'], + 2996 => ['MMLMVLI', 'MMXMVI', 'MMVMI'], + 2997 => ['MMLMVLII', 'MMXMVII', 'MMVMII'], + 2998 => ['MMLMVLIII', 'MMXMVIII', 'MMVMIII'], + 2999 => ['MMLMVLIV', 'MMXMIX', 'MMVMIV', 'MMIM'], + 3045 => ['MMMVL'], + 3046 => ['MMMVLI'], + 3047 => ['MMMVLII'], + 3048 => ['MMMVLIII'], + 3049 => ['MMMVLIV', 'MMMIL'], + 3095 => ['MMMVC'], + 3096 => ['MMMVCI'], + 3097 => ['MMMVCII'], + 3098 => ['MMMVCIII'], + 3099 => ['MMMVCIV', 'MMMIC'], + 3145 => ['MMMCVL'], + 3146 => ['MMMCVLI'], + 3147 => ['MMMCVLII'], + 3148 => ['MMMCVLIII'], + 3149 => ['MMMCVLIV', 'MMMCIL'], + 3195 => ['MMMCVC'], + 3196 => ['MMMCVCI'], + 3197 => ['MMMCVCII'], + 3198 => ['MMMCVCIII'], + 3199 => ['MMMCVCIV', 'MMMCIC'], + 3245 => ['MMMCCVL'], + 3246 => ['MMMCCVLI'], + 3247 => ['MMMCCVLII'], + 3248 => ['MMMCCVLIII'], + 3249 => ['MMMCCVLIV', 'MMMCCIL'], + 3295 => ['MMMCCVC'], + 3296 => ['MMMCCVCI'], + 3297 => ['MMMCCVCII'], + 3298 => ['MMMCCVCIII'], + 3299 => ['MMMCCVCIV', 'MMMCCIC'], + 3345 => ['MMMCCCVL'], + 3346 => ['MMMCCCVLI'], + 3347 => ['MMMCCCVLII'], + 3348 => ['MMMCCCVLIII'], + 3349 => ['MMMCCCVLIV', 'MMMCCCIL'], + 3395 => ['MMMCCCVC'], + 3396 => ['MMMCCCVCI'], + 3397 => ['MMMCCCVCII'], + 3398 => ['MMMCCCVCIII'], + 3399 => ['MMMCCCVCIV', 'MMMCCCIC'], + 3445 => ['MMMCDVL'], + 3446 => ['MMMCDVLI'], + 3447 => ['MMMCDVLII'], + 3448 => ['MMMCDVLIII'], + 3449 => ['MMMCDVLIV', 'MMMCDIL'], + 3450 => ['MMMLD'], + 3451 => ['MMMLDI'], + 3452 => ['MMMLDII'], + 3453 => ['MMMLDIII'], + 3454 => ['MMMLDIV'], + 3455 => ['MMMLDV'], + 3456 => ['MMMLDVI'], + 3457 => ['MMMLDVII'], + 3458 => ['MMMLDVIII'], + 3459 => ['MMMLDIX'], + 3460 => ['MMMLDX'], + 3461 => ['MMMLDXI'], + 3462 => ['MMMLDXII'], + 3463 => ['MMMLDXIII'], + 3464 => ['MMMLDXIV'], + 3465 => ['MMMLDXV'], + 3466 => ['MMMLDXVI'], + 3467 => ['MMMLDXVII'], + 3468 => ['MMMLDXVIII'], + 3469 => ['MMMLDXIX'], + 3470 => ['MMMLDXX'], + 3471 => ['MMMLDXXI'], + 3472 => ['MMMLDXXII'], + 3473 => ['MMMLDXXIII'], + 3474 => ['MMMLDXXIV'], + 3475 => ['MMMLDXXV'], + 3476 => ['MMMLDXXVI'], + 3477 => ['MMMLDXXVII'], + 3478 => ['MMMLDXXVIII'], + 3479 => ['MMMLDXXIX'], + 3480 => ['MMMLDXXX'], + 3481 => ['MMMLDXXXI'], + 3482 => ['MMMLDXXXII'], + 3483 => ['MMMLDXXXIII'], + 3484 => ['MMMLDXXXIV'], + 3485 => ['MMMLDXXXV'], + 3486 => ['MMMLDXXXVI'], + 3487 => ['MMMLDXXXVII'], + 3488 => ['MMMLDXXXVIII'], + 3489 => ['MMMLDXXXIX'], + 3490 => ['MMMLDXL', 'MMMXD'], + 3491 => ['MMMLDXLI', 'MMMXDI'], + 3492 => ['MMMLDXLII', 'MMMXDII'], + 3493 => ['MMMLDXLIII', 'MMMXDIII'], + 3494 => ['MMMLDXLIV', 'MMMXDIV'], + 3495 => ['MMMLDVL', 'MMMXDV', 'MMMVD'], + 3496 => ['MMMLDVLI', 'MMMXDVI', 'MMMVDI'], + 3497 => ['MMMLDVLII', 'MMMXDVII', 'MMMVDII'], + 3498 => ['MMMLDVLIII', 'MMMXDVIII', 'MMMVDIII'], + 3499 => ['MMMLDVLIV', 'MMMXDIX', 'MMMVDIV', 'MMMID'], + 3545 => ['MMMDVL'], + 3546 => ['MMMDVLI'], + 3547 => ['MMMDVLII'], + 3548 => ['MMMDVLIII'], + 3549 => ['MMMDVLIV', 'MMMDIL'], + 3595 => ['MMMDVC'], + 3596 => ['MMMDVCI'], + 3597 => ['MMMDVCII'], + 3598 => ['MMMDVCIII'], + 3599 => ['MMMDVCIV', 'MMMDIC'], + 3645 => ['MMMDCVL'], + 3646 => ['MMMDCVLI'], + 3647 => ['MMMDCVLII'], + 3648 => ['MMMDCVLIII'], + 3649 => ['MMMDCVLIV', 'MMMDCIL'], + 3695 => ['MMMDCVC'], + 3696 => ['MMMDCVCI'], + 3697 => ['MMMDCVCII'], + 3698 => ['MMMDCVCIII'], + 3699 => ['MMMDCVCIV', 'MMMDCIC'], + 3745 => ['MMMDCCVL'], + 3746 => ['MMMDCCVLI'], + 3747 => ['MMMDCCVLII'], + 3748 => ['MMMDCCVLIII'], + 3749 => ['MMMDCCVLIV', 'MMMDCCIL'], + 3795 => ['MMMDCCVC'], + 3796 => ['MMMDCCVCI'], + 3797 => ['MMMDCCVCII'], + 3798 => ['MMMDCCVCIII'], + 3799 => ['MMMDCCVCIV', 'MMMDCCIC'], + 3845 => ['MMMDCCCVL'], + 3846 => ['MMMDCCCVLI'], + 3847 => ['MMMDCCCVLII'], + 3848 => ['MMMDCCCVLIII'], + 3849 => ['MMMDCCCVLIV', 'MMMDCCCIL'], + 3895 => ['MMMDCCCVC'], + 3896 => ['MMMDCCCVCI'], + 3897 => ['MMMDCCCVCII'], + 3898 => ['MMMDCCCVCIII'], + 3899 => ['MMMDCCCVCIV', 'MMMDCCCIC'], + 3945 => ['MMMCMVL'], + 3946 => ['MMMCMVLI'], + 3947 => ['MMMCMVLII'], + 3948 => ['MMMCMVLIII'], + 3949 => ['MMMCMVLIV', 'MMMCMIL'], + 3950 => ['MMMLM'], + 3951 => ['MMMLMI'], + 3952 => ['MMMLMII'], + 3953 => ['MMMLMIII'], + 3954 => ['MMMLMIV'], + 3955 => ['MMMLMV'], + 3956 => ['MMMLMVI'], + 3957 => ['MMMLMVII'], + 3958 => ['MMMLMVIII'], + 3959 => ['MMMLMIX'], + 3960 => ['MMMLMX'], + 3961 => ['MMMLMXI'], + 3962 => ['MMMLMXII'], + 3963 => ['MMMLMXIII'], + 3964 => ['MMMLMXIV'], + 3965 => ['MMMLMXV'], + 3966 => ['MMMLMXVI'], + 3967 => ['MMMLMXVII'], + 3968 => ['MMMLMXVIII'], + 3969 => ['MMMLMXIX'], + 3970 => ['MMMLMXX'], + 3971 => ['MMMLMXXI'], + 3972 => ['MMMLMXXII'], + 3973 => ['MMMLMXXIII'], + 3974 => ['MMMLMXXIV'], + 3975 => ['MMMLMXXV'], + 3976 => ['MMMLMXXVI'], + 3977 => ['MMMLMXXVII'], + 3978 => ['MMMLMXXVIII'], + 3979 => ['MMMLMXXIX'], + 3980 => ['MMMLMXXX'], + 3981 => ['MMMLMXXXI'], + 3982 => ['MMMLMXXXII'], + 3983 => ['MMMLMXXXIII'], + 3984 => ['MMMLMXXXIV'], + 3985 => ['MMMLMXXXV'], + 3986 => ['MMMLMXXXVI'], + 3987 => ['MMMLMXXXVII'], + 3988 => ['MMMLMXXXVIII'], + 3989 => ['MMMLMXXXIX'], + 3990 => ['MMMLMXL', 'MMMXM'], + 3991 => ['MMMLMXLI', 'MMMXMI'], + 3992 => ['MMMLMXLII', 'MMMXMII'], + 3993 => ['MMMLMXLIII', 'MMMXMIII'], + 3994 => ['MMMLMXLIV', 'MMMXMIV'], + 3995 => ['MMMLMVL', 'MMMXMV', 'MMMVM'], + 3996 => ['MMMLMVLI', 'MMMXMVI', 'MMMVMI'], + 3997 => ['MMMLMVLII', 'MMMXMVII', 'MMMVMII'], + 3998 => ['MMMLMVLIII', 'MMMXMVIII', 'MMMVMIII'], + 3999 => ['MMMLMVLIV', 'MMMXMIX', 'MMMVMIV', 'MMMIM'], + ]; + + private const THOUSANDS = ['', 'M', 'MM', 'MMM']; + private const HUNDREDS = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM']; + private const TENS = ['', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC']; + private const ONES = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX']; + const MAX_ROMAN_VALUE = 3999; + const MAX_ROMAN_STYLE = 4; + + private static function valueOk(int $aValue, int $style): string + { + $origValue = $aValue; + $m = \intdiv($aValue, 1000); + $aValue %= 1000; + $c = \intdiv($aValue, 100); + $aValue %= 100; + $t = \intdiv($aValue, 10); + $aValue %= 10; + $result = self::THOUSANDS[$m] . self::HUNDREDS[$c] . self::TENS[$t] . self::ONES[$aValue]; + if ($style > 0) { + if (array_key_exists($origValue, self::VALUES)) { + $arr = self::VALUES[$origValue]; + $idx = min($style, count($arr)) - 1; + $result = $arr[$idx]; + } + } + + return $result; + } + + private static function styleOk(int $aValue, int $style): string + { + return ($aValue < 0 || $aValue > self::MAX_ROMAN_VALUE) ? ExcelError::VALUE() : self::valueOk($aValue, $style); + } + + public static function calculateRoman(int $aValue, int $style): string + { + return ($style < 0 || $style > self::MAX_ROMAN_STYLE) ? ExcelError::VALUE() : self::styleOk($aValue, $style); + } + + /** + * ROMAN. + * + * Converts a number to Roman numeral + * + * @param mixed $aValue Number to convert + * Or can be an array of numbers + * @param mixed $style Number indicating one of five possible forms + * Or can be an array of styles + * + * @return array|string Roman numeral, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate(mixed $aValue, mixed $style = 0): array|string + { + if (is_array($aValue) || is_array($style)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $aValue, $style); + } + + try { + $aValue = Helpers::validateNumericNullBool($aValue); + if (is_bool($style)) { + $style = $style ? 0 : 4; + } + $style = Helpers::validateNumericNullSubstitution($style, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::calculateRoman((int) $aValue, (int) $style); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Round.php new file mode 100644 index 0000000..d8e338c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Round.php @@ -0,0 +1,240 @@ +|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function round(mixed $number, mixed $precision): array|string|float + { + if (is_array($number) || is_array($precision)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $precision); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $precision = Helpers::validateNumericNullBool($precision); + } catch (Exception $e) { + return $e->getMessage(); + } + + return round($number, (int) $precision); + } + + /** + * ROUNDUP. + * + * Rounds a number up to a specified number of decimal places + * + * @param array|float $number Number to round, or can be an array of numbers + * @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function up($number, $digits): array|string|float + { + if (is_array($number) || is_array($digits)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $digits); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $digits = (int) Helpers::validateNumericNullSubstitution($digits, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($number == 0.0) { + return 0.0; + } + + if (PHP_VERSION_ID >= 80400) { + return round( + (float) (string) $number, + $digits, + RoundingMode::AwayFromZero //* @phpstan-ignore-line + ); + } + + // @codeCoverageIgnoreStart + if ($number < 0.0) { + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); + } + + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); + // @codeCoverageIgnoreEnd + } + + /** + * ROUNDDOWN. + * + * Rounds a number down to a specified number of decimal places + * + * @param null|array|float|string $number Number to round, or can be an array of numbers + * @param array|float|int|string $digits Number of digits to which you want to round $number, or can be an array of numbers + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function down($number, $digits): array|string|float + { + if (is_array($number) || is_array($digits)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $digits); + } + + try { + $number = Helpers::validateNumericNullBool($number); + $digits = (int) Helpers::validateNumericNullSubstitution($digits, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($number == 0.0) { + return 0.0; + } + + if (PHP_VERSION_ID >= 80400) { + return round( + (float) (string) $number, + $digits, + RoundingMode::TowardsZero //* @phpstan-ignore-line + ); + } + + // @codeCoverageIgnoreStart + if ($number < 0.0) { + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); + } + + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); + // @codeCoverageIgnoreEnd + } + + /** + * MROUND. + * + * Rounds a number to the nearest multiple of a specified value + * + * @param mixed $number Expect float. Number to round, or can be an array of numbers + * @param mixed $multiple Expect int. Multiple to which you want to round, or can be an array of numbers. + * + * @return array|float|int|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function multiple(mixed $number, mixed $multiple): array|string|int|float + { + if (is_array($number) || is_array($multiple)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $multiple); + } + + try { + $number = Helpers::validateNumericNullSubstitution($number, 0); + $multiple = Helpers::validateNumericNullSubstitution($multiple, null); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($number == 0 || $multiple == 0) { + return 0; + } + if ((Helpers::returnSign($number)) == (Helpers::returnSign($multiple))) { + $multiplier = 1 / $multiple; + + return round($number * $multiplier) / $multiplier; + } + + return ExcelError::NAN(); + } + + /** + * EVEN. + * + * Returns number rounded up to the nearest even integer. + * You can use this function for processing items that come in twos. For example, + * a packing crate accepts rows of one or two items. The crate is full when + * the number of items, rounded up to the nearest two, matches the crate's + * capacity. + * + * Excel Function: + * EVEN(number) + * + * @param array|float $number Number to round, or can be an array of numbers + * + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function even($number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::getEven($number); + } + + /** + * ODD. + * + * Returns number rounded up to the nearest odd integer. + * + * @param array|float $number Number to round, or can be an array of numbers + * + * @return array|float|int|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function odd($number): array|string|int|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + $significance = Helpers::returnSign($number); + if ($significance == 0) { + return 1; + } + + $result = ceil($number / $significance) * $significance; + if ($result == Helpers::getEven($result)) { + $result += $significance; + } + + return $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php new file mode 100644 index 0000000..fee7bc4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php @@ -0,0 +1,53 @@ +|float|int|string The result, or a string containing an error + */ + public static function evaluate(mixed $x, mixed $n, mixed $m, ...$args): array|string|float|int + { + if (is_array($x) || is_array($n) || is_array($m)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 3, $x, $n, $m, ...$args); + } + + try { + $x = Helpers::validateNumericNullSubstitution($x, 0); + $n = Helpers::validateNumericNullSubstitution($n, 0); + $m = Helpers::validateNumericNullSubstitution($m, 0); + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + $returnValue = 0; + $i = 0; + foreach ($aArgs as $argx) { + if ($argx !== null) { + $arg = Helpers::validateNumericNullSubstitution($argx, 0); + $returnValue += $arg * $x ** ($n + ($m * $i)); + ++$i; + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php new file mode 100644 index 0000000..5d2fb97 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php @@ -0,0 +1,38 @@ +|float $number Number to round, or can be an array of numbers + * + * @return array|int|string sign value, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate($number): array|string|int + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::returnSign($number); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php new file mode 100644 index 0000000..60c152e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php @@ -0,0 +1,64 @@ +|float|string square root + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function sqrt(mixed $number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(sqrt($number)); + } + + /** + * SQRTPI. + * + * Returns the square root of (number * pi). + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string Square Root of Number * Pi, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function pi($number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullSubstitution($number, 0); + Helpers::validateNotNegative($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return sqrt($number * M_PI); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php new file mode 100644 index 0000000..09f9cfa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php @@ -0,0 +1,139 @@ +getWorksheet()->getRowDimension((int) $row)->getVisible(); + }, + ARRAY_FILTER_USE_KEY + ); + } + + /** + * @param mixed[] $args + * + * @return mixed[] + */ + protected static function filterFormulaArgs(Cell $cellReference, array $args): array + { + return array_filter( + $args, + function ($index) use ($cellReference): bool { + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + $column = $explodeArray[2] ?? ''; + $retVal = true; + if ($cellReference->getWorksheet()->cellExists($column . $row)) { + //take this cell out if it contains the SUBTOTAL or AGGREGATE functions in a formula + $isFormula = $cellReference->getWorksheet()->getCell($column . $row)->isFormula(); + $cellFormula = !preg_match( + '/^=.*\b(SUBTOTAL|AGGREGATE)\s*\(/i', + $cellReference->getWorksheet()->getCell($column . $row)->getValueString() + ); + + $retVal = !$isFormula || $cellFormula; + } + + return $retVal; + }, + ARRAY_FILTER_USE_KEY + ); + } + + /** + * @var array + */ + private const CALL_FUNCTIONS = [ + 1 => [Statistical\Averages::class, 'average'], // 1 and 101 + [Statistical\Counts::class, 'COUNT'], // 2 and 102 + [Statistical\Counts::class, 'COUNTA'], // 3 and 103 + [Statistical\Maximum::class, 'max'], // 4 and 104 + [Statistical\Minimum::class, 'min'], // 5 and 105 + [Operations::class, 'product'], // 6 and 106 + [Statistical\StandardDeviations::class, 'STDEV'], // 7 and 107 + [Statistical\StandardDeviations::class, 'STDEVP'], // 8 and 108 + [Sum::class, 'sumIgnoringStrings'], // 9 and 109 + [Statistical\Variances::class, 'VAR'], // 10 and 110 + [Statistical\Variances::class, 'VARP'], // 111 and 111 + ]; + + /** + * SUBTOTAL. + * + * Returns a subtotal in a list or database. + * + * @param mixed $functionType + * A number 1 to 11 that specifies which function to + * use in calculating subtotals within a range + * list + * Numbers 101 to 111 shadow the functions of 1 to 11 + * but ignore any values in the range that are + * in hidden rows + * @param mixed[] $args A mixed data series of values + */ + public static function evaluate(mixed $functionType, ...$args): float|int|string + { + /** @var Cell */ + $cellReference = array_pop($args); + $bArgs = Functions::flattenArrayIndexed($args); + $aArgs = []; + // int keys must come before string keys for PHP 8.0+ + // Otherwise, PHP thinks positional args follow keyword + // in the subsequent call to call_user_func_array. + // Fortunately, order of args is unimportant to Subtotal. + foreach ($bArgs as $key => $value) { + if (is_int($key)) { + $aArgs[$key] = $value; + } + } + foreach ($bArgs as $key => $value) { + if (!is_int($key)) { + $aArgs[$key] = $value; + } + } + + try { + $subtotal = (int) Helpers::validateNumericNullBool($functionType); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Calculate + if ($subtotal > 100) { + $aArgs = self::filterHiddenArgs($cellReference, $aArgs); + $subtotal -= 100; + } + + $aArgs = self::filterFormulaArgs($cellReference, $aArgs); + if (array_key_exists($subtotal, self::CALL_FUNCTIONS)) { + $call = self::CALL_FUNCTIONS[$subtotal]; + + return call_user_func_array($call, $aArgs); //* @phpstan-ignore-line + } + + return ExcelError::VALUE(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php new file mode 100644 index 0000000..b5d0339 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -0,0 +1,115 @@ +|float|int|string + */ + public static function sumErroringStrings(mixed ...$args): float|int|string|array + { + $returnValue = 0; + // Loop through the arguments + $aArgs = Functions::flattenArrayIndexed($args); + foreach ($aArgs as $k => $arg) { + // Is it a numeric value? + if (is_numeric($arg)) { + $returnValue += $arg; + } elseif (is_bool($arg)) { + $returnValue += (int) $arg; + } elseif (ErrorValue::isError($arg)) { + /** @var string $arg */ + return $arg; + } elseif ($arg !== null && !Functions::isCellValue($k)) { + // ignore non-numerics from cell, but fail as literals (except null) + return ExcelError::VALUE(); + } + } + + return $returnValue; + } + + /** + * SUMPRODUCT. + * + * Excel Function: + * SUMPRODUCT(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|int|string The result, or a string containing an error + */ + public static function product(mixed ...$args): string|int|float + { + $arrayList = $args; + + $wrkArray = Functions::flattenArray(array_shift($arrayList)); + $wrkCellCount = count($wrkArray); + + for ($i = 0; $i < $wrkCellCount; ++$i) { + if ((!is_numeric($wrkArray[$i])) || (is_string($wrkArray[$i]))) { + $wrkArray[$i] = 0; + } + } + + foreach ($arrayList as $matrixData) { + $array2 = Functions::flattenArray($matrixData); + $count = count($array2); + if ($wrkCellCount != $count) { + return ExcelError::VALUE(); + } + + foreach ($array2 as $i => $val) { + if ((!is_numeric($val)) || (is_string($val))) { + $val = 0; + } + /** @var array $wrkArray */ + $wrkArray[$i] *= $val; + } + } + + return array_sum($wrkArray); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php new file mode 100644 index 0000000..f37be22 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php @@ -0,0 +1,143 @@ +getMessage(); + } + + return $returnValue; + } + + /** + * @param mixed[] $array1 + * @param mixed[] $array2 + */ + private static function getCount(array $array1, array $array2): int + { + $count = count($array1); + if ($count !== count($array2)) { + throw new Exception(ExcelError::NA()); + } + + return $count; + } + + /** + * These functions accept only numeric arguments, not even strings which are numeric. + */ + private static function numericNotString(mixed $item): bool + { + return is_numeric($item) && !is_string($item); + } + + /** + * SUMX2MY2. + * + * @param mixed[] $matrixData1 Matrix #1 + * @param mixed[] $matrixData2 Matrix #2 + */ + public static function sumXSquaredMinusYSquared(array $matrixData1, array $matrixData2): string|int|float + { + try { + /** @var array */ + $array1 = Functions::flattenArray($matrixData1); + /** @var array */ + $array2 = Functions::flattenArray($matrixData2); + $count = self::getCount($array1, $array2); + + $result = 0; + for ($i = 0; $i < $count; ++$i) { + if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { + $result += ($array1[$i] * $array1[$i]) - ($array2[$i] * $array2[$i]); + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $result; + } + + /** + * SUMX2PY2. + * + * @param mixed[] $matrixData1 Matrix #1 + * @param mixed[] $matrixData2 Matrix #2 + */ + public static function sumXSquaredPlusYSquared(array $matrixData1, array $matrixData2): string|int|float + { + try { + /** @var array */ + $array1 = Functions::flattenArray($matrixData1); + /** @var array */ + $array2 = Functions::flattenArray($matrixData2); + $count = self::getCount($array1, $array2); + + $result = 0; + for ($i = 0; $i < $count; ++$i) { + if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { + $result += ($array1[$i] * $array1[$i]) + ($array2[$i] * $array2[$i]); + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $result; + } + + /** + * SUMXMY2. + * + * @param mixed[] $matrixData1 Matrix #1 + * @param mixed[] $matrixData2 Matrix #2 + */ + public static function sumXMinusYSquared(array $matrixData1, array $matrixData2): string|int|float + { + try { + /** @var array */ + $array1 = Functions::flattenArray($matrixData1); + /** @var array */ + $array2 = Functions::flattenArray($matrixData2); + $count = self::getCount($array1, $array2); + + $result = 0; + for ($i = 0; $i < $count; ++$i) { + if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { + $result += ($array1[$i] - $array2[$i]) * ($array1[$i] - $array2[$i]); + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php new file mode 100644 index 0000000..b131b04 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php @@ -0,0 +1,64 @@ +|float $angle Number, or can be an array of numbers + * + * @return array|float|string The cosecant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function csc($angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(1.0, sin($angle)); + } + + /** + * CSCH. + * + * Returns the hyperbolic cosecant of an angle. + * + * @param array|float $angle Number, or can be an array of numbers + * + * @return array|float|string The hyperbolic cosecant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function csch($angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(1.0, sinh($angle)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php new file mode 100644 index 0000000..010a1c2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php @@ -0,0 +1,116 @@ +|float|string cosine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function cos(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return cos($number); + } + + /** + * COSH. + * + * Returns the result of builtin function cosh after validating args. + * + * @param mixed $number Should be numeric, or can be an array of numbers + * + * @return array|float|string hyperbolic cosine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function cosh(mixed $number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return cosh($number); + } + + /** + * ACOS. + * + * Returns the arccosine of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The arccosine of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function acos($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(acos($number)); + } + + /** + * ACOSH. + * + * Returns the arc inverse hyperbolic cosine of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The inverse hyperbolic cosine of the number, or an error string + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function acosh($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(acosh($number)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php new file mode 100644 index 0000000..34b4c00 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php @@ -0,0 +1,118 @@ +|float $angle Number, or can be an array of numbers + * + * @return array|float|string The cotangent of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function cot($angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(cos($angle), sin($angle)); + } + + /** + * COTH. + * + * Returns the hyperbolic cotangent of an angle. + * + * @param array|float $angle Number, or can be an array of numbers + * + * @return array|float|string The hyperbolic cotangent of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function coth($angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(1.0, tanh($angle)); + } + + /** + * ACOT. + * + * Returns the arccotangent of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The arccotangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function acot($number): array|string|float + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (M_PI / 2) - atan($number); + } + + /** + * ACOTH. + * + * Returns the hyperbolic arccotangent of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The hyperbolic arccotangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function acoth($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + $result = ($number === 1) ? NAN : (log(($number + 1) / ($number - 1)) / 2); + + return Helpers::numberOrNan($result); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php new file mode 100644 index 0000000..6c9d317 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php @@ -0,0 +1,64 @@ +|float $angle Number, or can be an array of numbers + * + * @return array|float|string The secant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function sec($angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(1.0, cos($angle)); + } + + /** + * SECH. + * + * Returns the hyperbolic secant of an angle. + * + * @param array|float $angle Number, or can be an array of numbers + * + * @return array|float|string The hyperbolic secant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function sech($angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(1.0, cosh($angle)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php new file mode 100644 index 0000000..5aad809 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php @@ -0,0 +1,116 @@ +|float|string sine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function sin(mixed $angle): array|string|float + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return sin($angle); + } + + /** + * SINH. + * + * Returns the result of builtin function sinh after validating args. + * + * @param mixed $angle Should be numeric, or can be an array of numbers + * + * @return array|float|string hyperbolic sine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function sinh(mixed $angle): array|string|float + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return sinh($angle); + } + + /** + * ASIN. + * + * Returns the arcsine of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The arcsine of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function asin($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(asin($number)); + } + + /** + * ASINH. + * + * Returns the inverse hyperbolic sine of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The inverse hyperbolic sine of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function asinh($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(asinh($number)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php new file mode 100644 index 0000000..a243bb7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php @@ -0,0 +1,160 @@ +|float|string tangent + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function tan(mixed $angle) + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::verySmallDenominator(sin($angle), cos($angle)); + } + + /** + * TANH. + * + * Returns the result of builtin function sinh after validating args. + * + * @param mixed $angle Should be numeric, or can be an array of numbers + * + * @return array|float|string hyperbolic tangent + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function tanh(mixed $angle): array|string|float + { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + + try { + $angle = Helpers::validateNumericNullBool($angle); + } catch (Exception $e) { + return $e->getMessage(); + } + + return tanh($angle); + } + + /** + * ATAN. + * + * Returns the arctangent of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The arctangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function atan($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(atan($number)); + } + + /** + * ATANH. + * + * Returns the inverse hyperbolic tangent of a number. + * + * @param array|float $number Number, or can be an array of numbers + * + * @return array|float|string The inverse hyperbolic tangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function atanh($number) + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return Helpers::numberOrNan(atanh($number)); + } + + /** + * ATAN2. + * + * This function calculates the arc tangent of the two variables x and y. It is similar to + * calculating the arc tangent of y ÷ x, except that the signs of both arguments are used + * to determine the quadrant of the result. + * The arctangent is the angle from the x-axis to a line containing the origin (0, 0) and a + * point with coordinates (xCoordinate, yCoordinate). The angle is given in radians between + * -pi and pi, excluding -pi. + * + * Note that the Excel ATAN2() function accepts its arguments in the reverse order to the standard + * PHP atan2() function, so we need to reverse them here before calling the PHP atan() function. + * + * Excel Function: + * ATAN2(xCoordinate,yCoordinate) + * + * @param mixed $xCoordinate should be float, the x-coordinate of the point, or can be an array of numbers + * @param mixed $yCoordinate should be float, the y-coordinate of the point, or can be an array of numbers + * + * @return array|float|string The inverse tangent of the specified x- and y-coordinates, or a string containing an error + * If an array of numbers is passed as one of the arguments, then the returned result will also be an array + * with the same dimensions + */ + public static function atan2(mixed $xCoordinate, mixed $yCoordinate): array|string|float + { + if (is_array($xCoordinate) || is_array($yCoordinate)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $xCoordinate, $yCoordinate); + } + + try { + $xCoordinate = Helpers::validateNumericNullBool($xCoordinate); + $yCoordinate = Helpers::validateNumericNullBool($yCoordinate); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($xCoordinate == 0) && ($yCoordinate == 0)) { + return ExcelError::DIV0(); + } + + return atan2($yCoordinate, $xCoordinate); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php new file mode 100644 index 0000000..e032c20 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php @@ -0,0 +1,36 @@ +|float|string $value Or can be an array of values + * @param array|float|int|string $digits Or can be an array of values + * + * @return array|float|string Truncated value, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function evaluate(array|float|string|null $value = 0, array|float|int|string $digits = 0): array|float|string + { + if (is_array($value) || is_array($digits)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $digits); + } + + return Round::down($value, $digits); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php new file mode 100644 index 0000000..a10b278 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php @@ -0,0 +1,59 @@ + $arg) { + $arg = self::testAcceptedBoolean($arg, $k); + // Is it a numeric value? + // Strings containing numeric values are only counted if they are string literals (not cell values) + // and then only in MS Excel and in Open Office, not in Gnumeric + if ((is_string($arg)) && (!is_numeric($arg)) && (!Functions::isCellValue($k))) { + return ExcelError::VALUE(); + } + if (self::isAcceptedCountable($arg, $k)) { + /** @var float|int|numeric-string $arg */ + /** @var float|int|numeric-string $aMean */ + $returnValue += abs($arg - $aMean); + ++$aCount; + } + } + + // Return + if ($aCount === 0) { + return ExcelError::DIV0(); + } + + return $returnValue / $aCount; + } + + /** + * AVERAGE. + * + * Returns the average (arithmetic mean) of the arguments + * + * Excel Function: + * AVERAGE(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|int|string (string if result is an error) + */ + public static function average(mixed ...$args): string|int|float + { + $returnValue = $aCount = 0; + + // Loop through arguments + foreach (Functions::flattenArrayIndexed($args) as $k => $arg) { + $arg = self::testAcceptedBoolean($arg, $k); + // Is it a numeric value? + // Strings containing numeric values are only counted if they are string literals (not cell values) + // and then only in MS Excel and in Open Office, not in Gnumeric + if ((is_string($arg)) && (!is_numeric($arg)) && (!Functions::isCellValue($k))) { + return ExcelError::VALUE(); + } + if (self::isAcceptedCountable($arg, $k)) { + /** @var float|int|numeric-string $arg */ + $returnValue += $arg; + ++$aCount; + } + } + + // Return + if ($aCount > 0) { + return $returnValue / $aCount; + } + + return ExcelError::DIV0(); + } + + /** + * AVERAGEA. + * + * Returns the average of its arguments, including numbers, text, and logical values + * + * Excel Function: + * AVERAGEA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|int|string (string if result is an error) + */ + public static function averageA(mixed ...$args): string|int|float + { + $returnValue = null; + + $aCount = 0; + // Loop through arguments + foreach (Functions::flattenArrayIndexed($args) as $k => $arg) { + if (is_numeric($arg)) { + // do nothing + } elseif (is_bool($arg)) { + $arg = (int) $arg; + } elseif (!Functions::isMatrixValue($k)) { + $arg = 0; + } else { + return ExcelError::VALUE(); + } + $returnValue += $arg; + ++$aCount; + } + + if ($aCount > 0) { + return $returnValue / $aCount; + } + + return ExcelError::DIV0(); + } + + /** + * MEDIAN. + * + * Returns the median of the given numbers. The median is the number in the middle of a set of numbers. + * + * Excel Function: + * MEDIAN(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function median(mixed ...$args): float|string + { + $aArgs = Functions::flattenArray($args); + + $returnValue = ExcelError::NAN(); + + /** @var array */ + $aArgs = self::filterArguments($aArgs); + $valueCount = count($aArgs); + if ($valueCount > 0) { + sort($aArgs, SORT_NUMERIC); + $valueCount = $valueCount / 2; + if ($valueCount == floor($valueCount)) { + $returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2; //* @phpstan-ignore-line + } else { + $valueCount = (int) floor($valueCount); + $returnValue = $aArgs[$valueCount]; + } + } + + return $returnValue; + } + + /** + * MODE. + * + * Returns the most frequently occurring, or repetitive, value in an array or range of data + * + * Excel Function: + * MODE(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function mode(mixed ...$args): float|string + { + $returnValue = ExcelError::NA(); + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + $aArgs = self::filterArguments($aArgs); + + if (!empty($aArgs)) { + return self::modeCalc($aArgs); + } + + return $returnValue; + } + + /** + * @param mixed[] $args + * + * @return mixed[] + */ + protected static function filterArguments(array $args): array + { + return array_filter( + $args, + function ($value): bool { + // Is it a numeric value? + return is_numeric($value) && (!is_string($value)); + } + ); + } + + /** + * Special variant of array_count_values that isn't limited to strings and integers, + * but can work with floating point numbers as values. + * + * @param mixed[] $data + */ + private static function modeCalc(array $data): float|string + { + $frequencyArray = []; + $index = 0; + $maxfreq = 0; + $maxfreqkey = ''; + $maxfreqdatum = ''; + foreach ($data as $datum) { + /** @var float|string $datum */ + $found = false; + ++$index; + foreach ($frequencyArray as $key => $value) { + /** @var string[] $value */ + if ((string) $value['value'] == (string) $datum) { + ++$frequencyArray[$key]['frequency']; + $freq = $frequencyArray[$key]['frequency']; + if ($freq > $maxfreq) { + $maxfreq = $freq; + $maxfreqkey = $key; + $maxfreqdatum = $datum; + } elseif ($freq == $maxfreq) { + if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore-line + $maxfreqkey = $key; + $maxfreqdatum = $datum; + } + } + $found = true; + + break; + } + } + + if ($found === false) { + $frequencyArray[] = [ + 'value' => $datum, + 'frequency' => 1, + 'index' => $index, + ]; + } + } + + if ($maxfreq <= 1) { + return ExcelError::NA(); + } + + return $maxfreqdatum; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php new file mode 100644 index 0000000..2051675 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php @@ -0,0 +1,126 @@ + 0)) { + $aCount = Counts::COUNT($aArgs); + if (Minimum::min($aArgs) > 0) { + return $aMean ** (1 / $aCount); + } + } + + return ExcelError::NAN(); + } + + /** + * HARMEAN. + * + * Returns the harmonic mean of a data set. The harmonic mean is the reciprocal of the + * arithmetic mean of reciprocals. + * + * Excel Function: + * HARMEAN(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + */ + public static function harmonic(mixed ...$args): string|float|int + { + // Loop through arguments + $aArgs = Functions::flattenArray($args); + if (Minimum::min($aArgs) < 0) { + return ExcelError::NAN(); + } + + $returnValue = 0; + $aCount = 0; + foreach ($aArgs as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + if ($arg <= 0) { + return ExcelError::NAN(); + } + $returnValue += (1 / $arg); + ++$aCount; + } + } + + // Return + if ($aCount > 0) { + return 1 / ($returnValue / $aCount); + } + + return ExcelError::NA(); + } + + /** + * TRIMMEAN. + * + * Returns the mean of the interior of a data set. TRIMMEAN calculates the mean + * taken by excluding a percentage of data points from the top and bottom tails + * of a data set. + * + * Excel Function: + * TRIMEAN(value1[,value2[, ...]], $discard) + * + * @param mixed $args Data values + */ + public static function trim(mixed ...$args): float|string + { + $aArgs = Functions::flattenArray($args); + + // Calculate + $percent = array_pop($aArgs); + + if ((is_numeric($percent)) && (!is_string($percent))) { + if (($percent < 0) || ($percent > 1)) { + return ExcelError::NAN(); + } + + $mArgs = []; + foreach ($aArgs as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $mArgs[] = $arg; + } + } + + $discard = floor(Counts::COUNT($mArgs) * $percent / 2); + sort($mArgs); + + for ($i = 0; $i < $discard; ++$i) { + array_pop($mArgs); + array_shift($mArgs); + } + + return Averages::average($mArgs); + } + + return ExcelError::VALUE(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php new file mode 100644 index 0000000..818d9db --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php @@ -0,0 +1,358 @@ + $value !== null && $value !== '' + ); + + $range = array_merge([[self::CONDITION_COLUMN_NAME]], array_chunk($range, 1)); + $condition = Functions::flattenSingleValue($condition); + $condition = array_merge([[self::CONDITION_COLUMN_NAME]], [[$condition]]); + + return DCount::evaluate($range, null, $condition, false); + } + + /** + * COUNTIFS. + * + * Counts the number of cells that contain numbers within the list of arguments + * + * Excel Function: + * COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + */ + public static function COUNTIFS(mixed ...$args): int|string + { + if (empty($args)) { + return 0; + } elseif (count($args) === 2) { + return self::COUNTIF(...$args); + } + + $database = self::buildDatabase(...$args); + $conditions = self::buildConditionSet(...$args); + + return DCount::evaluate($database, null, $conditions, false); + } + + /** + * MAXIFS. + * + * Returns the maximum value within a range of cells that contain numbers within the list of arguments + * + * Excel Function: + * MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + */ + public static function MAXIFS(mixed ...$args): null|float|string + { + if (empty($args)) { + return 0.0; + } + + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); + + return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions, false); + } + + /** + * MINIFS. + * + * Returns the minimum value within a range of cells that contain numbers within the list of arguments + * + * Excel Function: + * MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + */ + public static function MINIFS(mixed ...$args): null|float|string + { + if (empty($args)) { + return 0.0; + } + + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); + + return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions, false); + } + + /** + * SUMIF. + * + * Totals the values of cells that contain numbers within the list of arguments + * + * Excel Function: + * SUMIF(range, criteria, [sum_range]) + * + * @param mixed $range Data values, expecting array + * @param mixed $sumRange Data values, expecting array + */ + public static function SUMIF(mixed $range, mixed $condition, mixed $sumRange = []): null|float|string + { + if ( + !is_array($range) + || array_key_exists(0, $range) + || !is_array($sumRange) + || array_key_exists(0, $sumRange) + ) { + $refError = ExcelError::REF(); + if (in_array($refError, [$range, $sumRange], true)) { + return $refError; + } + + throw new CalcException('Must specify range of cells, not any kind of literal'); + } + $database = self::databaseFromRangeAndValue($range, $sumRange); + $condition = Functions::flattenSingleValue($condition); + $condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]]; + + return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $condition); + } + + /** + * SUMIFS. + * + * Counts the number of cells that contain numbers within the list of arguments + * + * Excel Function: + * SUMIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + */ + public static function SUMIFS(mixed ...$args): null|float|string + { + if (empty($args)) { + return 0.0; + } elseif (count($args) === 3) { + return self::SUMIF($args[1], $args[2], $args[0]); + } + + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); + + return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); + } + + /** + * @param mixed[] $args + * + * @return mixed[][] + */ + private static function buildConditionSet(...$args): array + { + $conditions = self::buildConditions(1, ...$args); + + return array_map(null, ...$conditions); + } + + /** + * @param mixed[] $args + * + * @return mixed[][] + */ + private static function buildConditionSetForValueRange(...$args): array + { + $conditions = self::buildConditions(2, ...$args); + + if (count($conditions) === 1) { + return array_map( + fn ($value): array => [$value], + $conditions[0] + ); + } + + return array_map(null, ...$conditions); + } + + /** + * @param mixed[] $args + * + * @return mixed[][] + */ + private static function buildConditions(int $startOffset, ...$args): array + { + $conditions = []; + + $pairCount = 1; + $argumentCount = count($args); + for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) { + $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [$args[$argument]]); + ++$pairCount; + } + + return $conditions; + } + + /** + * @param mixed[] $args + * + * @return mixed[] + */ + private static function buildDatabase(...$args): array + { + $database = []; + + return self::buildDataSet(0, $database, ...$args); + } + + /** + * @param mixed[] $args + * + * @return mixed[] + */ + private static function buildDatabaseWithValueRange(...$args): array + { + $database = []; + $database[] = array_merge( + [self::VALUE_COLUMN_NAME], + Functions::flattenArray($args[0]) + ); + + return self::buildDataSet(1, $database, ...$args); + } + + /** + * @param mixed[][] $database + * @param mixed[] $args + * + * @return mixed[] + */ + private static function buildDataSet(int $startOffset, array $database, ...$args): array + { + $pairCount = 1; + $argumentCount = count($args); + for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) { + $database[] = array_merge( + [sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], + Functions::flattenArray($args[$argument]) + ); + ++$pairCount; + } + + return array_map(null, ...$database); + } + + /** + * @param mixed[] $range + * @param mixed[] $valueRange + * + * @return mixed[] + */ + private static function databaseFromRangeAndValue(array $range, array $valueRange = []): array + { + $range = Functions::flattenArray($range); + + $valueRange = Functions::flattenArray($valueRange); + if (empty($valueRange)) { + $valueRange = $range; + } + + $database = array_map(null, array_merge([self::CONDITION_COLUMN_NAME], $range), array_merge([self::VALUE_COLUMN_NAME], $valueRange)); + + return $database; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php new file mode 100644 index 0000000..fe3232d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php @@ -0,0 +1,54 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function CONFIDENCE(mixed $alpha, mixed $stdDev, mixed $size) + { + if (is_array($alpha) || is_array($stdDev) || is_array($size)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $alpha, $stdDev, $size); + } + + try { + $alpha = StatisticalValidations::validateFloat($alpha); + $stdDev = StatisticalValidations::validateFloat($stdDev); + $size = StatisticalValidations::validateInt($size); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($alpha <= 0) || ($alpha >= 1) || ($stdDev <= 0) || ($size < 1)) { + return ExcelError::NAN(); + } + /** @var float $temp */ + $temp = Distributions\StandardNormal::inverse(1 - $alpha / 2); + + /** @var float */ + $result = Functions::scalar($temp * $stdDev / sqrt($size)); + + return $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Counts.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Counts.php new file mode 100644 index 0000000..20ed634 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Counts.php @@ -0,0 +1,96 @@ + $arg) { + $arg = self::testAcceptedBoolean($arg, $k); + // Is it a numeric value? + // Strings containing numeric values are only counted if they are string literals (not cell values) + // and then only in MS Excel and in Open Office, not in Gnumeric + if (self::isAcceptedCountable($arg, $k, true)) { + ++$returnValue; + } + } + + return $returnValue; + } + + /** + * COUNTA. + * + * Counts the number of cells that are not empty within the list of arguments + * + * Excel Function: + * COUNTA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + */ + public static function COUNTA(mixed ...$args): int + { + $returnValue = 0; + + // Loop through arguments + $aArgs = Functions::flattenArrayIndexed($args); + foreach ($aArgs as $k => $arg) { + // Nulls are counted if literals, but not if cell values + if ($arg !== null || (!Functions::isCellValue($k))) { + ++$returnValue; + } + } + + return $returnValue; + } + + /** + * COUNTBLANK. + * + * Counts the number of empty cells within the list of arguments + * + * Excel Function: + * COUNTBLANK(value1[,value2[, ...]]) + * + * @param mixed $range Data values + */ + public static function COUNTBLANK(mixed $range): int + { + if ($range === null) { + return 1; + } + if (!is_array($range) || array_key_exists(0, $range)) { + throw new CalcException('Must specify range of cells, not any kind of literal'); + } + $returnValue = 0; + + // Loop through arguments + $aArgs = Functions::flattenArray($range); + foreach ($aArgs as $arg) { + // Is it a blank cell? + if (($arg === null) || ((is_string($arg)) && ($arg == ''))) { + ++$returnValue; + } + } + + return $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php new file mode 100644 index 0000000..e6ffe8d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php @@ -0,0 +1,138 @@ + $arg) { + // Is it a numeric value? + if ( + (is_bool($arg)) + && ((!Functions::isCellValue($k)) + || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) + ) { + $arg = (int) $arg; + } + if ((is_numeric($arg)) && (!is_string($arg))) { + $returnValue += ($arg - $aMean) ** 2; + ++$aCount; + } + } + + return $aCount === 0 ? ExcelError::VALUE() : $returnValue; + } + + /** + * KURT. + * + * Returns the kurtosis of a data set. Kurtosis characterizes the relative peakedness + * or flatness of a distribution compared with the normal distribution. Positive + * kurtosis indicates a relatively peaked distribution. Negative kurtosis indicates a + * relatively flat distribution. + * + * @param mixed[] ...$args Data Series + */ + public static function kurtosis(...$args): string|int|float + { + $aArgs = Functions::flattenArrayIndexed($args); + $mean = Averages::average($aArgs); + if (!is_numeric($mean)) { + return ExcelError::DIV0(); + } + $stdDev = (float) StandardDeviations::STDEV($aArgs); + + if ($stdDev > 0) { + $count = $summer = 0; + + foreach ($aArgs as $k => $arg) { + if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $summer += (($arg - $mean) / $stdDev) ** 4; + ++$count; + } + } + } + + if ($count > 3) { + return $summer * ($count * ($count + 1) + / (($count - 1) * ($count - 2) * ($count - 3))) - (3 * ($count - 1) ** 2 + / (($count - 2) * ($count - 3))); + } + } + + return ExcelError::DIV0(); + } + + /** + * SKEW. + * + * Returns the skewness of a distribution. Skewness characterizes the degree of asymmetry + * of a distribution around its mean. Positive skewness indicates a distribution with an + * asymmetric tail extending toward more positive values. Negative skewness indicates a + * distribution with an asymmetric tail extending toward more negative values. + * + * @param mixed[] ...$args Data Series + * + * @return float|int|string The result, or a string containing an error + */ + public static function skew(...$args): string|int|float + { + $aArgs = Functions::flattenArrayIndexed($args); + $mean = Averages::average($aArgs); + if (!is_numeric($mean)) { + return ExcelError::DIV0(); + } + $stdDev = StandardDeviations::STDEV($aArgs); + if ($stdDev === 0.0 || is_string($stdDev)) { + return ExcelError::DIV0(); + } + + $count = $summer = 0; + // Loop through arguments + foreach ($aArgs as $k => $arg) { + if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { + } elseif (!is_numeric($arg)) { + return ExcelError::VALUE(); + } else { + // Is it a numeric value? + if (!is_string($arg)) { + $summer += (($arg - $mean) / $stdDev) ** 3; + ++$count; + } + } + } + + if ($count > 2) { + return $summer * ($count / (($count - 1) * ($count - 2))); + } + + return ExcelError::DIV0(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php new file mode 100644 index 0000000..b53f15c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php @@ -0,0 +1,279 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $alpha, mixed $beta, mixed $rMin = 0.0, mixed $rMax = 1.0): array|string|float + { + if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($rMin) || is_array($rMax)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $rMin, $rMax); + } + + $rMin = $rMin ?? 0.0; + $rMax = $rMax ?? 1.0; + + try { + $value = DistributionValidations::validateFloat($value); + $alpha = DistributionValidations::validateFloat($alpha); + $beta = DistributionValidations::validateFloat($beta); + $rMax = DistributionValidations::validateFloat($rMax); + $rMin = DistributionValidations::validateFloat($rMin); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($rMin > $rMax) { + $tmp = $rMin; + $rMin = $rMax; + $rMax = $tmp; + } + if (($value < $rMin) || ($value > $rMax) || ($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax)) { + return ExcelError::NAN(); + } + + $value -= $rMin; + $value /= ($rMax - $rMin); + + return self::incompleteBeta($value, $alpha, $beta); + } + + /** + * BETAINV. + * + * Returns the inverse of the Beta distribution. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $alpha Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $beta Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $rMin Minimum value as a float + * Or can be an array of values + * @param mixed $rMax Maximum value as a float + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $probability, mixed $alpha, mixed $beta, mixed $rMin = 0.0, mixed $rMax = 1.0): array|string|float + { + if (is_array($probability) || is_array($alpha) || is_array($beta) || is_array($rMin) || is_array($rMax)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta, $rMin, $rMax); + } + + $rMin = $rMin ?? 0.0; + $rMax = $rMax ?? 1.0; + + try { + $probability = DistributionValidations::validateProbability($probability); + $alpha = DistributionValidations::validateFloat($alpha); + $beta = DistributionValidations::validateFloat($beta); + $rMax = DistributionValidations::validateFloat($rMax); + $rMin = DistributionValidations::validateFloat($rMin); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($rMin > $rMax) { + $tmp = $rMin; + $rMin = $rMax; + $rMax = $tmp; + } + if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) { + return ExcelError::NAN(); + } + + return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax); + } + + private static function calculateInverse(float $probability, float $alpha, float $beta, float $rMin, float $rMax): string|float + { + $a = 0; + $b = 2; + $guess = ($a + $b) / 2; + + $i = 0; + while ((($b - $a) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { + $guess = ($a + $b) / 2; + $result = self::distribution($guess, $alpha, $beta); + if (($result === $probability) || ($result === 0.0)) { + $b = $a; + } elseif ($result > $probability) { + $b = $guess; + } else { + $a = $guess; + } + } + + if ($i === self::MAX_ITERATIONS) { + return ExcelError::NA(); + } + + return round($rMin + $guess * ($rMax - $rMin), 12); + } + + /** + * Incomplete beta function. + * + * @author Jaco van Kooten + * @author Paul Meagher + * + * The computation is based on formulas from Numerical Recipes, Chapter 6.4 (W.H. Press et al, 1992). + * + * @param float $x require 0<=x<=1 + * @param float $p require p>0 + * @param float $q require q>0 + * + * @return float 0 if x<0, p<=0, q<=0 or p+q>2.55E305 and 1 if x>1 to avoid errors and over/underflow + */ + public static function incompleteBeta(float $x, float $p, float $q): float + { + if ($x <= 0.0) { + return 0.0; + } elseif ($x >= 1.0) { + return 1.0; + } elseif (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) { + return 0.0; + } + + $beta_gam = exp((0 - self::logBeta($p, $q)) + $p * log($x) + $q * log(1.0 - $x)); + if ($x < ($p + 1.0) / ($p + $q + 2.0)) { + return $beta_gam * self::betaFraction($x, $p, $q) / $p; + } + + return 1.0 - ($beta_gam * self::betaFraction(1 - $x, $q, $p) / $q); + } + + // Function cache for logBeta function + + private static float $logBetaCacheP = 0.0; + + private static float $logBetaCacheQ = 0.0; + + private static float $logBetaCacheResult = 0.0; + + /** + * The natural logarithm of the beta function. + * + * @param float $p require p>0 + * @param float $q require q>0 + * + * @return float 0 if p<=0, q<=0 or p+q>2.55E305 to avoid errors and over/underflow + * + * @author Jaco van Kooten + */ + private static function logBeta(float $p, float $q): float + { + if ($p != self::$logBetaCacheP || $q != self::$logBetaCacheQ) { + self::$logBetaCacheP = $p; + self::$logBetaCacheQ = $q; + if (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) { + self::$logBetaCacheResult = 0.0; + } else { + self::$logBetaCacheResult = Gamma::logGamma($p) + Gamma::logGamma($q) - Gamma::logGamma($p + $q); + } + } + + return self::$logBetaCacheResult; + } + + /** + * Evaluates of continued fraction part of incomplete beta function. + * Based on an idea from Numerical Recipes (W.H. Press et al, 1992). + * + * @author Jaco van Kooten + */ + private static function betaFraction(float $x, float $p, float $q): float + { + $c = 1.0; + $sum_pq = $p + $q; + $p_plus = $p + 1.0; + $p_minus = $p - 1.0; + $h = 1.0 - $sum_pq * $x / $p_plus; + if (abs($h) < self::XMININ) { + $h = self::XMININ; + } + $h = 1.0 / $h; + $frac = $h; + $m = 1; + $delta = 0.0; + while ($m <= self::MAX_ITERATIONS && abs($delta - 1.0) > Functions::PRECISION) { + $m2 = 2 * $m; + // even index for d + $d = $m * ($q - $m) * $x / (($p_minus + $m2) * ($p + $m2)); + $h = 1.0 + $d * $h; + if (abs($h) < self::XMININ) { + $h = self::XMININ; + } + $h = 1.0 / $h; + $c = 1.0 + $d / $c; + if (abs($c) < self::XMININ) { + $c = self::XMININ; + } + $frac *= $h * $c; + // odd index for d + $d = -($p + $m) * ($sum_pq + $m) * $x / (($p + $m2) * ($p_plus + $m2)); + $h = 1.0 + $d * $h; + if (abs($h) < self::XMININ) { + $h = self::XMININ; + } + $h = 1.0 / $h; + $c = 1.0 + $d / $c; + if (abs($c) < self::XMININ) { + $c = self::XMININ; + } + $delta = $h * $c; + $frac *= $delta; + ++$m; + } + + return $frac; + } + + /* + private static function betaValue(float $a, float $b): float + { + return (Gamma::gammaValue($a) * Gamma::gammaValue($b)) / + Gamma::gammaValue($a + $b); + } + + private static function regularizedIncompleteBeta(float $value, float $a, float $b): float + { + return self::incompleteBeta($value, $a, $b) / self::betaValue($a, $b); + } + */ +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php new file mode 100644 index 0000000..948e640 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php @@ -0,0 +1,231 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $trials, mixed $probability, mixed $cumulative) + { + if (is_array($value) || is_array($trials) || is_array($probability) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $trials, $probability, $cumulative); + } + + try { + $value = DistributionValidations::validateInt($value); + $trials = DistributionValidations::validateInt($trials); + $probability = DistributionValidations::validateProbability($probability); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($value > $trials)) { + return ExcelError::NAN(); + } + + if ($cumulative) { + return self::calculateCumulativeBinomial($value, $trials, $probability); + } + /** @var float $comb */ + $comb = Combinations::withoutRepetition($trials, $value); + + return $comb * $probability ** $value + * (1 - $probability) ** ($trials - $value); + } + + /** + * BINOM.DIST.RANGE. + * + * Returns the Binomial Distribution probability for the number of successes from a specified number + * of trials falling into a specified range. + * + * @param mixed $trials Integer number of trials + * Or can be an array of values + * @param mixed $probability Probability of success on each trial as a float + * Or can be an array of values + * @param mixed $successes The integer number of successes in trials + * Or can be an array of values + * @param mixed $limit Upper limit for successes in trials as null, or an integer + * If null, then this will indicate the same as the number of Successes + * Or can be an array of values + * + * @return array|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function range(mixed $trials, mixed $probability, mixed $successes, mixed $limit = null): array|string|float|int + { + if (is_array($trials) || is_array($probability) || is_array($successes) || is_array($limit)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $successes, $limit); + } + + $limit = $limit ?? $successes; + + try { + $trials = DistributionValidations::validateInt($trials); + $probability = DistributionValidations::validateProbability($probability); + $successes = DistributionValidations::validateInt($successes); + $limit = DistributionValidations::validateInt($limit); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($successes < 0) || ($successes > $trials)) { + return ExcelError::NAN(); + } + if (($limit < 0) || ($limit > $trials) || $limit < $successes) { + return ExcelError::NAN(); + } + + $summer = 0; + for ($i = $successes; $i <= $limit; ++$i) { + /** @var float $comb */ + $comb = Combinations::withoutRepetition($trials, $i); + $summer += $comb * $probability ** $i + * (1 - $probability) ** ($trials - $i); + } + + return $summer; + } + + /** + * NEGBINOMDIST. + * + * Returns the negative binomial distribution. NEGBINOMDIST returns the probability that + * there will be number_f failures before the number_s-th success, when the constant + * probability of a success is probability_s. This function is similar to the binomial + * distribution, except that the number of successes is fixed, and the number of trials is + * variable. Like the binomial, trials are assumed to be independent. + * + * @param mixed $failures Number of Failures as an integer + * Or can be an array of values + * @param mixed $successes Threshold number of Successes as an integer + * Or can be an array of values + * @param mixed $probability Probability of success on each trial as a float + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + * + * TODO Add support for the cumulative flag not present for NEGBINOMDIST, but introduced for NEGBINOM.DIST + * The cumulative default should be false to reflect the behaviour of NEGBINOMDIST + */ + public static function negative(mixed $failures, mixed $successes, mixed $probability): array|string|float + { + if (is_array($failures) || is_array($successes) || is_array($probability)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $failures, $successes, $probability); + } + + try { + $failures = DistributionValidations::validateInt($failures); + $successes = DistributionValidations::validateInt($successes); + $probability = DistributionValidations::validateProbability($probability); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($failures < 0) || ($successes < 1)) { + return ExcelError::NAN(); + } + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + if (($failures + $successes - 1) <= 0) { + return ExcelError::NAN(); + } + } + /** @var float $comb */ + $comb = Combinations::withoutRepetition($failures + $successes - 1, $successes - 1); + + return $comb + * ($probability ** $successes) * ((1 - $probability) ** $failures); + } + + /** + * BINOM.INV. + * + * Returns the smallest value for which the cumulative binomial distribution is greater + * than or equal to a criterion value + * + * @param mixed $trials number of Bernoulli trials as an integer + * Or can be an array of values + * @param mixed $probability probability of a success on each trial as a float + * Or can be an array of values + * @param mixed $alpha criterion value as a float + * Or can be an array of values + * + * @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $trials, mixed $probability, mixed $alpha): array|string|int + { + if (is_array($trials) || is_array($probability) || is_array($alpha)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $alpha); + } + + try { + $trials = DistributionValidations::validateInt($trials); + $probability = DistributionValidations::validateProbability($probability); + $alpha = DistributionValidations::validateFloat($alpha); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($trials < 0) { + return ExcelError::NAN(); + } elseif (($alpha < 0.0) || ($alpha > 1.0)) { + return ExcelError::NAN(); + } + + $successes = 0; + while ($successes <= $trials) { + $result = self::calculateCumulativeBinomial($successes, $trials, $probability); + if ($result >= $alpha) { + break; + } + ++$successes; + } + + return $successes; + } + + private static function calculateCumulativeBinomial(int $value, int $trials, float $probability): float|int + { + $summer = 0; + for ($i = 0; $i <= $value; ++$i) { + /** @var float $comb */ + $comb = Combinations::withoutRepetition($trials, $i); + $summer += $comb * $probability ** $i + * (1 - $probability) ** ($trials - $i); + } + + return $summer; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php new file mode 100644 index 0000000..b132484 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -0,0 +1,332 @@ +|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distributionRightTail(mixed $value, mixed $degrees): array|string|int|float + { + if (is_array($value) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees); + } + + try { + $value = DistributionValidations::validateFloat($value); + $degrees = DistributionValidations::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($degrees < 1) { + return ExcelError::NAN(); + } + if ($value < 0) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return 1; + } + + return ExcelError::NAN(); + } + + return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2)); + } + + /** + * CHIDIST. + * + * Returns the one-tailed probability of the chi-squared distribution. + * + * @param mixed $value Float value for which we want the probability + * Or can be an array of values + * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distributionLeftTail(mixed $value, mixed $degrees, mixed $cumulative): array|string|int|float + { + if (is_array($value) || is_array($degrees) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $degrees = DistributionValidations::validateInt($degrees); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($degrees < 1) { + return ExcelError::NAN(); + } + if ($value < 0) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return 1; + } + + return ExcelError::NAN(); + } + + if ($cumulative === true) { + $temp = self::distributionRightTail($value, $degrees); + + return 1 - (is_numeric($temp) ? $temp : 0); + } + + return ($value ** (($degrees / 2) - 1) * exp(-$value / 2)) + / ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2)); + } + + /** + * CHIINV. + * + * Returns the inverse of the right-tailed probability of the chi-squared distribution. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverseRightTail(mixed $probability, mixed $degrees) + { + if (is_array($probability) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $degrees = DistributionValidations::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($degrees < 1) { + return ExcelError::NAN(); + } + + $callback = fn (float $value): float => 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) + / Gamma::gammaValue($degrees / 2)); + + $newtonRaphson = new NewtonRaphson($callback); + + return $newtonRaphson->execute($probability); + } + + /** + * CHIINV. + * + * Returns the inverse of the left-tailed probability of the chi-squared distribution. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverseLeftTail(mixed $probability, mixed $degrees): array|string|float + { + if (is_array($probability) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $degrees = DistributionValidations::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($degrees < 1) { + return ExcelError::NAN(); + } + + return self::inverseLeftTailCalculation($probability, $degrees); + } + + /** + * CHITEST. + * + * Uses the chi-square test to calculate the probability that the differences between two supplied data sets + * (of observed and expected frequencies), are likely to be simply due to sampling error, + * or if they are likely to be real. + * + * @param float[] $actual an array of observed frequencies + * @param float[] $expected an array of expected frequencies + */ + public static function test($actual, $expected): float|string + { + $rows = count($actual); + /** @var float[] */ + $actual = Functions::flattenArray($actual); + /** @var float[] */ + $expected = Functions::flattenArray($expected); + $columns = intdiv(count($actual), $rows); + + $countActuals = count($actual); + $countExpected = count($expected); + if ($countActuals !== $countExpected || $countActuals === 1) { + return ExcelError::NAN(); + } + + $result = 0.0; + for ($i = 0; $i < $countActuals; ++$i) { + if ($expected[$i] == 0.0) { + return ExcelError::DIV0(); + } elseif ($expected[$i] < 0.0) { + return ExcelError::NAN(); + } + $result += (($actual[$i] - $expected[$i]) ** 2) / $expected[$i]; + } + + $degrees = self::degrees($rows, $columns); + + /** @var float|string */ + $result = Functions::scalar(self::distributionRightTail($result, $degrees)); + + return $result; + } + + protected static function degrees(int $rows, int $columns): int + { + if ($rows === 1) { + return $columns - 1; + } elseif ($columns === 1) { + return $rows - 1; + } + + return ($columns - 1) * ($rows - 1); + } + + private static function inverseLeftTailCalculation(float $probability, int $degrees): float + { + // bracket the root + $min = 0; + $sd = sqrt(2.0 * $degrees); + $max = 2 * $sd; + $s = -1; + + while ($s * self::pchisq($max, $degrees) > $probability * $s) { + $min = $max; + $max += 2 * $sd; + } + + // Find root using bisection + $chi2 = 0.5 * ($min + $max); + + while (($max - $min) > self::EPS * $chi2) { + if ($s * self::pchisq($chi2, $degrees) > $probability * $s) { + $min = $chi2; + } else { + $max = $chi2; + } + $chi2 = 0.5 * ($min + $max); + } + + return $chi2; + } + + private static function pchisq(float $chi2, int $degrees): float + { + return self::gammp($degrees, 0.5 * $chi2); + } + + private static function gammp(int $n, float $x): float + { + if ($x < 0.5 * $n + 1) { + return self::gser($n, $x); + } + + return 1 - self::gcf($n, $x); + } + + // Return the incomplete gamma function P(n/2,x) evaluated by + // series representation. Algorithm from numerical recipe. + // Assume that n is a positive integer and x>0, won't check arguments. + // Relative error controlled by the eps parameter + private static function gser(int $n, float $x): float + { + /** @var float $gln */ + $gln = Gamma::ln($n / 2); + $a = 0.5 * $n; + $ap = $a; + $sum = 1.0 / $a; + $del = $sum; + for ($i = 1; $i < 101; ++$i) { + ++$ap; + $del = $del * $x / $ap; + $sum += $del; + if ($del < $sum * self::EPS) { + break; + } + } + + return $sum * exp(-$x + $a * log($x) - $gln); + } + + // Return the incomplete gamma function Q(n/2,x) evaluated by + // its continued fraction representation. Algorithm from numerical recipe. + // Assume that n is a postive integer and x>0, won't check arguments. + // Relative error controlled by the eps parameter + private static function gcf(int $n, float $x): float + { + /** @var float $gln */ + $gln = Gamma::ln($n / 2); + $a = 0.5 * $n; + $b = $x + 1 - $a; + $fpmin = 1.e-300; + $c = 1 / $fpmin; + $d = 1 / $b; + $h = $d; + for ($i = 1; $i < 101; ++$i) { + $an = -$i * ($i - $a); + $b += 2; + $d = $an * $d + $b; + if (abs($d) < $fpmin) { + $d = $fpmin; + } + $c = $b + $an / $c; + if (abs($c) < $fpmin) { + $c = $fpmin; + } + $d = 1 / $d; + $del = $d * $c; + $h = $h * $del; + if (abs($del - 1) < self::EPS) { + break; + } + } + + return $h * exp(-$x + $a * log($x) - $gln); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php new file mode 100644 index 0000000..61c62f6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php @@ -0,0 +1,21 @@ + 1.0) { + throw new Exception(ExcelError::NAN()); + } + + return $probability; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php new file mode 100644 index 0000000..9acbf57 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php @@ -0,0 +1,54 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $lambda, mixed $cumulative): array|string|float + { + if (is_array($value) || is_array($lambda) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $lambda, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $lambda = DistributionValidations::validateFloat($lambda); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($lambda < 0)) { + return ExcelError::NAN(); + } + + if ($cumulative === true) { + return 1 - exp(0 - $value * $lambda); + } + + return $lambda * exp(0 - $value * $lambda); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php new file mode 100644 index 0000000..0127565 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php @@ -0,0 +1,63 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $u, mixed $v, mixed $cumulative): array|string|float + { + if (is_array($value) || is_array($u) || is_array($v) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $u, $v, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $u = DistributionValidations::validateInt($u); + $v = DistributionValidations::validateInt($v); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($value < 0 || $u < 1 || $v < 1) { + return ExcelError::NAN(); + } + + if ($cumulative) { + $adjustedValue = ($u * $value) / ($u * $value + $v); + + return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2); + } + + return (Gamma::gammaValue(($v + $u) / 2) + / (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2))) + * (($u / $v) ** ($u / 2)) + * (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2))); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php new file mode 100644 index 0000000..2036a2a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php @@ -0,0 +1,72 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value): array|string|float + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = DistributionValidations::validateFloat($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value <= -1) || ($value >= 1)) { + return ExcelError::NAN(); + } + + return 0.5 * log((1 + $value) / (1 - $value)); + } + + /** + * FISHERINV. + * + * Returns the inverse of the Fisher transformation. Use this transformation when + * analyzing correlations between ranges or arrays of data. If y = FISHER(x), then + * FISHERINV(y) = x. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $probability): array|string|float + { + if (is_array($probability)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $probability); + } + + try { + $probability = DistributionValidations::validateFloat($probability); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (exp(2 * $probability) - 1) / (exp(2 * $probability) + 1); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php new file mode 100644 index 0000000..60f5309 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php @@ -0,0 +1,148 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function gamma(mixed $value): array|string|float + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = DistributionValidations::validateFloat($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ((((int) $value) == ((float) $value)) && $value <= 0.0) { + return ExcelError::NAN(); + } + + return self::gammaValue($value); + } + + /** + * GAMMADIST. + * + * Returns the gamma distribution. + * + * @param mixed $value Float Value at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $a Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $b Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $a, mixed $b, mixed $cumulative) + { + if (is_array($value) || is_array($a) || is_array($b) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $a, $b, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $a = DistributionValidations::validateFloat($a); + $b = DistributionValidations::validateFloat($b); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($a <= 0) || ($b <= 0)) { + return ExcelError::NAN(); + } + + return self::calculateDistribution($value, $a, $b, $cumulative); + } + + /** + * GAMMAINV. + * + * Returns the inverse of the Gamma distribution. + * + * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values + * @param mixed $alpha Parameter to the distribution as a float + * Or can be an array of values + * @param mixed $beta Parameter to the distribution as a float + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $probability, mixed $alpha, mixed $beta) + { + if (is_array($probability) || is_array($alpha) || is_array($beta)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $alpha = DistributionValidations::validateFloat($alpha); + $beta = DistributionValidations::validateFloat($beta); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($alpha <= 0.0) || ($beta <= 0.0)) { + return ExcelError::NAN(); + } + + return self::calculateInverse($probability, $alpha, $beta); + } + + /** + * GAMMALN. + * + * Returns the natural logarithm of the gamma function. + * + * @param mixed $value Float Value at which you want to evaluate the distribution + * Or can be an array of values + * + * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function ln(mixed $value): array|string|float + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = DistributionValidations::validateFloat($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($value <= 0) { + return ExcelError::NAN(); + } + + return log(self::gammaValue($value)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php new file mode 100644 index 0000000..4ae6075 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php @@ -0,0 +1,382 @@ + Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $result = self::calculateDistribution($x, $alpha, $beta, true); + $error = $result - $probability; + + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + $pdf = self::calculateDistribution($x, $alpha, $beta, false); + // Avoid division by zero + if ($pdf !== 0.0) { + $dx = $error / $pdf; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($pdf == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i === self::MAX_ITERATIONS) { + return ExcelError::NA(); + } + + return $x; + } + + // + // Implementation of the incomplete Gamma function + // + public static function incompleteGamma(float $a, float $x): float + { + static $max = 32; + $summer = 0; + for ($n = 0; $n <= $max; ++$n) { + $divisor = $a; + for ($i = 1; $i <= $n; ++$i) { + $divisor *= ($a + $i); + } + $summer += ($x ** $n / $divisor); + } + + return $x ** $a * exp(0 - $x) * $summer; + } + + private const GAMMA_VALUE_P0 = 1.000000000190015; + private const GAMMA_VALUE_P = [ + 1 => 76.18009172947146, + 2 => -86.50532032941677, + 3 => 24.01409824083091, + 4 => -1.231739572450155, + 5 => 1.208650973866179e-3, + 6 => -5.395239384953e-6, + ]; + + // + // Implementation of the Gamma function + // + public static function gammaValue(float $value): float + { + if ($value == 0.0) { + return 0; + } + + $y = $x = $value; + $tmp = $x + 5.5; + $tmp -= ($x + 0.5) * log($tmp); + + $summer = self::GAMMA_VALUE_P0; + for ($j = 1; $j <= 6; ++$j) { + $summer += (self::GAMMA_VALUE_P[$j] / ++$y); + } + + return exp(0 - $tmp + log(self::SQRT2PI * $summer / $x)); + } + + private const LG_D1 = -0.5772156649015328605195174; + + private const LG_D2 = 0.4227843350984671393993777; + + private const LG_D4 = 1.791759469228055000094023; + + private const LG_P1 = [ + 4.945235359296727046734888, + 201.8112620856775083915565, + 2290.838373831346393026739, + 11319.67205903380828685045, + 28557.24635671635335736389, + 38484.96228443793359990269, + 26377.48787624195437963534, + 7225.813979700288197698961, + ]; + + private const LG_P2 = [ + 4.974607845568932035012064, + 542.4138599891070494101986, + 15506.93864978364947665077, + 184793.2904445632425417223, + 1088204.76946882876749847, + 3338152.967987029735917223, + 5106661.678927352456275255, + 3074109.054850539556250927, + ]; + + private const LG_P4 = [ + 14745.02166059939948905062, + 2426813.369486704502836312, + 121475557.4045093227939592, + 2663432449.630976949898078, + 29403789566.34553899906876, + 170266573776.5398868392998, + 492612579337.743088758812, + 560625185622.3951465078242, + ]; + + private const LG_Q1 = [ + 67.48212550303777196073036, + 1113.332393857199323513008, + 7738.757056935398733233834, + 27639.87074403340708898585, + 54993.10206226157329794414, + 61611.22180066002127833352, + 36351.27591501940507276287, + 8785.536302431013170870835, + ]; + + private const LG_Q2 = [ + 183.0328399370592604055942, + 7765.049321445005871323047, + 133190.3827966074194402448, + 1136705.821321969608938755, + 5267964.117437946917577538, + 13467014.54311101692290052, + 17827365.30353274213975932, + 9533095.591844353613395747, + ]; + + private const LG_Q4 = [ + 2690.530175870899333379843, + 639388.5654300092398984238, + 41355999.30241388052042842, + 1120872109.61614794137657, + 14886137286.78813811542398, + 101680358627.2438228077304, + 341747634550.7377132798597, + 446315818741.9713286462081, + ]; + + private const LG_C = [ + -0.001910444077728, + 8.4171387781295e-4, + -5.952379913043012e-4, + 7.93650793500350248e-4, + -0.002777777777777681622553, + 0.08333333333333333331554247, + 0.0057083835261, + ]; + + // Rough estimate of the fourth root of logGamma_xBig + private const LG_FRTBIG = 2.25e76; + + private const PNT68 = 0.6796875; + + // Function cache for logGamma + + private static float $logGammaCacheResult = 0.0; + + private static float $logGammaCacheX = 0.0; + + /** + * logGamma function. + * + * Original author was Jaco van Kooten. Ported to PHP by Paul Meagher. + * + * The natural logarithm of the gamma function.
+ * Based on public domain NETLIB (Fortran) code by W. J. Cody and L. Stoltz
+ * Applied Mathematics Division
+ * Argonne National Laboratory
+ * Argonne, IL 60439
+ *

+ * References: + *

    + *
  1. W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for the Natural + * Logarithm of the Gamma Function,' Math. Comp. 21, 1967, pp. 198-203.
  2. + *
  3. K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May, 1969.
  4. + *
  5. Hart, Et. Al., Computer Approximations, Wiley and sons, New York, 1968.
  6. + *
+ *

+ *

+ * From the original documentation: + *

+ *

+ * This routine calculates the LOG(GAMMA) function for a positive real argument X. + * Computation is based on an algorithm outlined in references 1 and 2. + * The program uses rational functions that theoretically approximate LOG(GAMMA) + * to at least 18 significant decimal digits. The approximation for X > 12 is from + * reference 3, while approximations for X < 12.0 are similar to those in reference + * 1, but are unpublished. The accuracy achieved depends on the arithmetic system, + * the compiler, the intrinsic functions, and proper selection of the + * machine-dependent constants. + *

+ *

+ * Error returns:
+ * The program returns the value XINF for X .LE. 0.0 or when overflow would occur. + * The computation is believed to be free of underflow and overflow. + *

+ * + * @version 1.1 + * + * @author Jaco van Kooten + * + * @return float MAX_VALUE for x < 0.0 or when overflow would occur, i.e. x > 2.55E305 + */ + public static function logGamma(float $x): float + { + if ($x == self::$logGammaCacheX) { + return self::$logGammaCacheResult; + } + + $y = $x; + if ($y > 0.0 && $y <= self::LOG_GAMMA_X_MAX_VALUE) { + if ($y <= self::EPS) { + $res = -log($y); + } elseif ($y <= 1.5) { + $res = self::logGamma1($y); + } elseif ($y <= 4.0) { + $res = self::logGamma2($y); + } elseif ($y <= 12.0) { + $res = self::logGamma3($y); + } else { + $res = self::logGamma4($y); + } + } else { + // -------------------------- + // Return for bad arguments + // -------------------------- + $res = self::MAX_VALUE; + } + + // ------------------------------ + // Final adjustments and return + // ------------------------------ + self::$logGammaCacheX = $x; + self::$logGammaCacheResult = $res; + + return $res; + } + + private static function logGamma1(float $y): float + { + // --------------------- + // EPS .LT. X .LE. 1.5 + // --------------------- + if ($y < self::PNT68) { + $corr = -log($y); + $xm1 = $y; + } else { + $corr = 0.0; + $xm1 = $y - 1.0; + } + + $xden = 1.0; + $xnum = 0.0; + if ($y <= 0.5 || $y >= self::PNT68) { + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm1 + self::LG_P1[$i]; + $xden = $xden * $xm1 + self::LG_Q1[$i]; + } + + return $corr + $xm1 * (self::LG_D1 + $xm1 * ($xnum / $xden)); + } + + $xm2 = $y - 1.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm2 + self::LG_P2[$i]; + $xden = $xden * $xm2 + self::LG_Q2[$i]; + } + + return $corr + $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden)); + } + + private static function logGamma2(float $y): float + { + // --------------------- + // 1.5 .LT. X .LE. 4.0 + // --------------------- + $xm2 = $y - 2.0; + $xden = 1.0; + $xnum = 0.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm2 + self::LG_P2[$i]; + $xden = $xden * $xm2 + self::LG_Q2[$i]; + } + + return $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden)); + } + + protected static function logGamma3(float $y): float + { + // ---------------------- + // 4.0 .LT. X .LE. 12.0 + // ---------------------- + $xm4 = $y - 4.0; + $xden = -1.0; + $xnum = 0.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm4 + self::LG_P4[$i]; + $xden = $xden * $xm4 + self::LG_Q4[$i]; + } + + return self::LG_D4 + $xm4 * ($xnum / $xden); + } + + protected static function logGamma4(float $y): float + { + // --------------------------------- + // Evaluate for argument .GE. 12.0 + // --------------------------------- + $res = 0.0; + if ($y <= self::LG_FRTBIG) { + $res = self::LG_C[6]; + $ysq = $y * $y; + for ($i = 0; $i < 6; ++$i) { + $res = $res / $ysq + self::LG_C[$i]; + } + $res /= $y; + $corr = log($y); + $res = $res + log(self::SQRT2PI) - 0.5 * $corr; + $res += $y * ($corr - 1.0); + } + + return $res; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php new file mode 100644 index 0000000..697b875 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php @@ -0,0 +1,75 @@ +|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $sampleSuccesses, mixed $sampleNumber, mixed $populationSuccesses, mixed $populationNumber): array|string|float + { + if ( + is_array($sampleSuccesses) || is_array($sampleNumber) + || is_array($populationSuccesses) || is_array($populationNumber) + ) { + return self::evaluateArrayArguments( + [self::class, __FUNCTION__], + $sampleSuccesses, + $sampleNumber, + $populationSuccesses, + $populationNumber + ); + } + + try { + $sampleSuccesses = DistributionValidations::validateInt($sampleSuccesses); + $sampleNumber = DistributionValidations::validateInt($sampleNumber); + $populationSuccesses = DistributionValidations::validateInt($populationSuccesses); + $populationNumber = DistributionValidations::validateInt($populationNumber); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($sampleSuccesses < 0) || ($sampleSuccesses > $sampleNumber) || ($sampleSuccesses > $populationSuccesses)) { + return ExcelError::NAN(); + } + if (($sampleNumber <= 0) || ($sampleNumber > $populationNumber)) { + return ExcelError::NAN(); + } + if (($populationSuccesses <= 0) || ($populationSuccesses > $populationNumber)) { + return ExcelError::NAN(); + } + + $successesPopulationAndSample = (float) Combinations::withoutRepetition($populationSuccesses, $sampleSuccesses); + $numbersPopulationAndSample = (float) Combinations::withoutRepetition($populationNumber, $sampleNumber); + $adjustedPopulationAndSample = (float) Combinations::withoutRepetition( + $populationNumber - $populationSuccesses, + $sampleNumber - $sampleSuccesses + ); + + return $successesPopulationAndSample * $adjustedPopulationAndSample / $numbersPopulationAndSample; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php new file mode 100644 index 0000000..a3309b4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php @@ -0,0 +1,139 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function cumulative(mixed $value, mixed $mean, mixed $stdDev) + { + if (is_array($value) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev); + } + + try { + $value = DistributionValidations::validateFloat($value); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value <= 0) || ($stdDev <= 0)) { + return ExcelError::NAN(); + } + + return StandardNormal::cumulative((log($value) - $mean) / $stdDev); + } + + /** + * LOGNORM.DIST. + * + * Returns the lognormal distribution of x, where ln(x) is normally distributed + * with parameters mean and standard_dev. + * + * @param mixed $value Float value for which we want the probability + * Or can be an array of values + * @param mixed $mean Mean value as a float + * Or can be an array of values + * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $mean, mixed $stdDev, mixed $cumulative = false) + { + if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value <= 0) || ($stdDev <= 0)) { + return ExcelError::NAN(); + } + + if ($cumulative === true) { + return StandardNormal::distribution((log($value) - $mean) / $stdDev, true); + } + + return (1 / (sqrt(2 * M_PI) * $stdDev * $value)) + * exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2))); + } + + /** + * LOGINV. + * + * Returns the inverse of the lognormal cumulative distribution + * + * @param mixed $probability Float probability for which we want the value + * Or can be an array of values + * @param mixed $mean Mean Value as a float + * Or can be an array of values + * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + * + * @TODO Try implementing P J Acklam's refinement algorithm for greater + * accuracy if I can get my head round the mathematics + * (as described at) http://home.online.no/~pjacklam/notes/invnorm/ + */ + public static function inverse(mixed $probability, mixed $mean, mixed $stdDev): array|string|float + { + if (is_array($probability) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev <= 0) { + return ExcelError::NAN(); + } + /** @var float $inverse */ + $inverse = StandardNormal::inverse($probability); + + return exp($mean + $stdDev * $inverse); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php new file mode 100644 index 0000000..b2348dc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php @@ -0,0 +1,68 @@ +callback = $callback; + } + + public function execute(float $probability): string|int|float + { + $xLo = 100; + $xHi = 0; + + $dx = 1; + $x = $xNew = 1; + $i = 0; + + while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $result = call_user_func($this->callback, $x); + if (!is_float($result)) { + return ExcelError::VALUE(); + } + $error = $result - $probability; + + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + // Avoid division by zero + if ($result != 0.0) { + $dx = $error / $result; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i == self::MAX_ITERATIONS) { + return ExcelError::NA(); + } + + return $x; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php new file mode 100644 index 0000000..5a0a7eb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php @@ -0,0 +1,184 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $mean, mixed $stdDev, mixed $cumulative): array|string|float + { + if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev < 0) { + return ExcelError::NAN(); + } + + if ($cumulative) { + return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2)))); + } + + return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev)))); + } + + /** + * NORMINV. + * + * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. + * + * @param mixed $probability Float probability for which we want the value + * Or can be an array of values + * @param mixed $mean Mean Value as a float + * Or can be an array of values + * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $probability, mixed $mean, mixed $stdDev): array|string|float + { + if (is_array($probability) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $mean = DistributionValidations::validateFloat($mean); + $stdDev = DistributionValidations::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev < 0) { + return ExcelError::NAN(); + } + + return (self::inverseNcdf($probability) * $stdDev) + $mean; + } + + /* + * inverse_ncdf.php + * ------------------- + * begin : Friday, January 16, 2004 + * copyright : (C) 2004 Michael Nickerson + * email : nickersonm@yahoo.com + * + */ + private static function inverseNcdf(float $p): float + { + // Inverse ncdf approximation by Peter J. Acklam, implementation adapted to + // PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as + // a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html + // I have not checked the accuracy of this implementation. Be aware that PHP + // will truncate the coeficcients to 14 digits. + + // You have permission to use and distribute this function freely for + // whatever purpose you want, but please show common courtesy and give credit + // where credit is due. + + // Input parameter is $p - probability - where 0 < p < 1. + + // Coefficients in rational approximations + /** @var array */ + static $a = [ + 1 => -3.969683028665376e+01, + 2 => 2.209460984245205e+02, + 3 => -2.759285104469687e+02, + 4 => 1.383577518672690e+02, + 5 => -3.066479806614716e+01, + 6 => 2.506628277459239e+00, + ]; + + /** @var array */ + static $b = [ + 1 => -5.447609879822406e+01, + 2 => 1.615858368580409e+02, + 3 => -1.556989798598866e+02, + 4 => 6.680131188771972e+01, + 5 => -1.328068155288572e+01, + ]; + + /** @var array */ + static $c = [ + 1 => -7.784894002430293e-03, + 2 => -3.223964580411365e-01, + 3 => -2.400758277161838e+00, + 4 => -2.549732539343734e+00, + 5 => 4.374664141464968e+00, + 6 => 2.938163982698783e+00, + ]; + + /** @var array */ + static $d = [ + 1 => 7.784695709041462e-03, + 2 => 3.224671290700398e-01, + 3 => 2.445134137142996e+00, + 4 => 3.754408661907416e+00, + ]; + + // Define lower and upper region break-points. + $p_low = 0.02425; //Use lower region approx. below this + $p_high = 1 - $p_low; //Use upper region approx. above this + + if (0 < $p && $p < $p_low) { + // Rational approximation for lower region. + $q = sqrt(-2 * log($p)); + + return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) + / (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); + } elseif ($p_high < $p && $p < 1) { + // Rational approximation for upper region. + $q = sqrt(-2 * log(1 - $p)); + + return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) + / (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); + } + + // Rational approximation for central region. + $q = $p - 0.5; + $r = $q * $q; + + return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q + / ((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php new file mode 100644 index 0000000..cf9a776 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php @@ -0,0 +1,66 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $mean, mixed $cumulative): array|string|float + { + if (is_array($value) || is_array($mean) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $mean = DistributionValidations::validateFloat($mean); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($mean < 0)) { + return ExcelError::NAN(); + } + + if ($cumulative) { + $summer = 0; + $floor = floor($value); + for ($i = 0; $i <= $floor; ++$i) { + /** @var float $fact */ + $fact = MathTrig\Factorial::fact($i); + $summer += $mean ** $i / $fact; + } + + return exp(0 - $mean) * $summer; + } + /** @var float $fact */ + $fact = MathTrig\Factorial::fact($value); + + return (exp(0 - $mean) * $mean ** $value) / $fact; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php new file mode 100644 index 0000000..474f3ec --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php @@ -0,0 +1,158 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function cumulative(mixed $value) + { + return Normal::distribution($value, 0, 1, true); + } + + /** + * NORM.S.DIST. + * + * Returns the standard normal cumulative distribution function. The distribution has + * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a + * table of standard normal curve areas. + * + * NOTE: We don't need to check for arrays to array-enable this function, because that is already + * handled by the logic in Normal::distribution() + * All we need to do is pass the value and cumulative through as scalar or as array. + * + * @param mixed $value Float value for which we want the probability + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $cumulative) + { + return Normal::distribution($value, 0, 1, $cumulative); + } + + /** + * NORMSINV. + * + * Returns the inverse of the standard normal cumulative distribution + * + * @param mixed $value float probability for which we want the value + * Or can be an array of values + * + * NOTE: We don't need to check for arrays to array-enable this function, because that is already + * handled by the logic in Normal::inverse() + * All we need to do is pass the value through as scalar or as array + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $value) + { + return Normal::inverse($value, 0, 1); + } + + /** + * GAUSS. + * + * Calculates the probability that a member of a standard normal population will fall between + * the mean and z standard deviations from the mean. + * + * @param mixed $value Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function gauss(mixed $value): array|string|float + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if (!is_numeric($value)) { + return ExcelError::VALUE(); + } + /** @var float $dist */ + $dist = self::distribution($value, true); + + return $dist - 0.5; + } + + /** + * ZTEST. + * + * Returns the one-tailed P-value of a z-test. + * + * For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be + * greater than the average of observations in the data set (array) — that is, the observed sample mean. + * + * @param mixed $dataSet The dataset should be an array of float values for the observations + * @param mixed $m0 Alpha Parameter + * Or can be an array of values + * @param mixed $sigma A null or float value for the Beta (Standard Deviation) Parameter; + * if null, we use the standard deviation of the dataset + * Or can be an array of values + * + * @return array|float|string (string if result is an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function zTest(mixed $dataSet, mixed $m0, mixed $sigma = null) + { + if (is_array($m0) || is_array($sigma)) { + return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $dataSet, $m0, $sigma); + } + + $dataSet = Functions::flattenArrayIndexed($dataSet); + + if (!is_numeric($m0) || ($sigma !== null && !is_numeric($sigma))) { + return ExcelError::VALUE(); + } + + if ($sigma === null) { + /** @var float $sigma */ + $sigma = StandardDeviations::STDEV($dataSet); + } + $n = count($dataSet); + + $sub1 = Averages::average($dataSet); + + if (!is_numeric($sub1)) { + return $sub1; + } + + $temp = self::cumulative(($sub1 - $m0) / ($sigma / sqrt($n))); + + return 1 - (is_numeric($temp) ? $temp : 0); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php new file mode 100644 index 0000000..cbe7d84 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php @@ -0,0 +1,132 @@ +|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $degrees, mixed $tails) + { + if (is_array($value) || is_array($degrees) || is_array($tails)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails); + } + + try { + $value = DistributionValidations::validateFloat($value); + $degrees = DistributionValidations::validateInt($degrees); + $tails = DistributionValidations::validateInt($tails); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($degrees < 1) || ($tails < 1) || ($tails > 2)) { + return ExcelError::NAN(); + } + + return self::calculateDistribution($value, $degrees, $tails); + } + + /** + * TINV. + * + * Returns the one-tailed probability of the chi-squared distribution. + * + * @param mixed $probability Float probability for the function + * Or can be an array of values + * @param mixed $degrees Integer value for degrees of freedom + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function inverse(mixed $probability, mixed $degrees) + { + if (is_array($probability) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); + } + + try { + $probability = DistributionValidations::validateProbability($probability); + $degrees = DistributionValidations::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($degrees <= 0) { + return ExcelError::NAN(); + } + + $callback = fn ($value) => self::distribution($value, $degrees, 2); + + $newtonRaphson = new NewtonRaphson($callback); + + return $newtonRaphson->execute($probability); + } + + private static function calculateDistribution(float $value, int $degrees, int $tails): float + { + // tdist, which finds the probability that corresponds to a given value + // of t with k degrees of freedom. This algorithm is translated from a + // pascal function on p81 of "Statistical Computing in Pascal" by D + // Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd: + // London). The above Pascal algorithm is itself a translation of the + // fortran algoritm "AS 3" by B E Cooper of the Atlas Computer + // Laboratory as reported in (among other places) "Applied Statistics + // Algorithms", editied by P Griffiths and I D Hill (1985; Ellis + // Horwood Ltd.; W. Sussex, England). + $tterm = $degrees; + $ttheta = atan2($value, sqrt($tterm)); + $tc = cos($ttheta); + $ts = sin($ttheta); + + if (($degrees % 2) === 1) { + $ti = 3; + $tterm = $tc; + } else { + $ti = 2; + $tterm = 1; + } + + $tsum = $tterm; + while ($ti < $degrees) { + $tterm *= $tc * $tc * ($ti - 1) / $ti; + $tsum += $tterm; + $ti += 2; + } + + $tsum *= $ts; + if (($degrees % 2) == 1) { + $tsum = Functions::M_2DIVPI * ($tsum + $ttheta); + } + + $tValue = 0.5 * (1 + $tsum); + if ($tails == 1) { + return 1 - abs($tValue); + } + + return 1 - abs((1 - $tValue) - $tValue); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php new file mode 100644 index 0000000..30ff2ef --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php @@ -0,0 +1,57 @@ +|float|string (string if result is an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function distribution(mixed $value, mixed $alpha, mixed $beta, mixed $cumulative): array|string|float + { + if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $cumulative); + } + + try { + $value = DistributionValidations::validateFloat($value); + $alpha = DistributionValidations::validateFloat($alpha); + $beta = DistributionValidations::validateFloat($beta); + $cumulative = DistributionValidations::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($alpha <= 0) || ($beta <= 0)) { + return ExcelError::NAN(); + } + + if ($cumulative) { + return 1 - exp(0 - ($value / $beta) ** $alpha); + } + + return ($alpha / $beta ** $alpha) * $value ** ($alpha - 1) * exp(0 - ($value / $beta) ** $alpha); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php new file mode 100644 index 0000000..a722fbd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php @@ -0,0 +1,17 @@ + $returnValue)) { + $returnValue = $arg; + } + } + } + + if ($returnValue === null) { + return 0; + } + /** @var float|int|string $returnValue */ + + return $returnValue; + } + + /** + * MAXA. + * + * Returns the greatest value in a list of arguments, including numbers, text, and logical values + * + * Excel Function: + * MAXA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + */ + public static function maxA(mixed ...$args): float|int|string + { + $returnValue = null; + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + foreach ($aArgs as $arg) { + if (ErrorValue::isError($arg)) { + $returnValue = $arg; + + break; + } + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + if (($returnValue === null) || ($arg > $returnValue)) { + $returnValue = $arg; + } + } + } + + if ($returnValue === null) { + return 0; + } + /** @var float|int|string $returnValue */ + + return $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php new file mode 100644 index 0000000..874edc3 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php @@ -0,0 +1,87 @@ +getMessage(); + } + + if (($entry < 0) || ($entry > 1)) { + return ExcelError::NAN(); + } + + $mArgs = self::percentileFilterValues($aArgs); + $mValueCount = count($mArgs); + if ($mValueCount > 0) { + sort($mArgs); + /** @var float[] $mArgs */ + $count = Counts::COUNT($mArgs); + $index = $entry * ($count - 1); + $indexFloor = floor($index); + $iBase = (int) $indexFloor; + if ($index == $indexFloor) { + return $mArgs[$iBase]; + } + $iNext = $iBase + 1; + $iProportion = $index - $iBase; + + return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion); + } + + return ExcelError::NAN(); + } + + /** + * PERCENTRANK. + * + * Returns the rank of a value in a data set as a percentage of the data set. + * Note that the returned rank is simply rounded to the appropriate significant digits, + * rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return + * 0.667 rather than 0.666 + * + * @param mixed $valueSet An array of (float) values, or a reference to, a list of numbers + * @param mixed $value The number whose rank you want to find + * @param mixed $significance The (integer) number of significant digits for the returned percentage value + * + * @return float|string (string if result is an error) + */ + public static function PERCENTRANK(mixed $valueSet, mixed $value, mixed $significance = 3): string|float + { + $valueSet = Functions::flattenArray($valueSet); + $value = Functions::flattenSingleValue($value); + $significance = ($significance === null) ? 3 : Functions::flattenSingleValue($significance); + + try { + $value = StatisticalValidations::validateFloat($value); + $significance = StatisticalValidations::validateInt($significance); + } catch (Exception $e) { + return $e->getMessage(); + } + + $valueSet = self::rankFilterValues($valueSet); + $valueCount = count($valueSet); + if ($valueCount == 0) { + return ExcelError::NA(); + } + sort($valueSet, SORT_NUMERIC); + + $valueAdjustor = $valueCount - 1; + if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) { + return ExcelError::NA(); + } + + $pos = array_search($value, $valueSet); + if ($pos === false) { + /** @var float[] $valueSet */ + $pos = 0; + $testValue = $valueSet[0]; + while ($testValue < $value) { + $testValue = $valueSet[++$pos]; + } + --$pos; + $pos += (($value - $valueSet[$pos]) / ($testValue - $valueSet[$pos])); + } + + return round(((float) $pos) / $valueAdjustor, $significance); + } + + /** + * QUARTILE. + * + * Returns the quartile of a data set. + * + * Excel Function: + * QUARTILE(value1[,value2[, ...]],entry) + * + * @param mixed $args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function QUARTILE(mixed ...$args) + { + $aArgs = Functions::flattenArray($args); + $entry = array_pop($aArgs); + + try { + $entry = StatisticalValidations::validateFloat($entry); + } catch (Exception $e) { + return $e->getMessage(); + } + + $entry = floor($entry); + $entry /= 4; + if (($entry < 0) || ($entry > 1)) { + return ExcelError::NAN(); + } + + return self::PERCENTILE($aArgs, $entry); + } + + /** + * RANK. + * + * Returns the rank of a number in a list of numbers. + * + * @param mixed $value The number whose rank you want to find + * @param mixed $valueSet An array of float values, or a reference to, a list of numbers + * @param mixed $order Order to sort the values in the value set + * + * @return float|string The result, or a string containing an error (0 = Descending, 1 = Ascending) + */ + public static function RANK(mixed $value, mixed $valueSet, mixed $order = self::RANK_SORT_DESCENDING) + { + $value = Functions::flattenSingleValue($value); + $valueSet = Functions::flattenArray($valueSet); + $order = ($order === null) ? self::RANK_SORT_DESCENDING : Functions::flattenSingleValue($order); + + try { + $value = StatisticalValidations::validateFloat($value); + $order = StatisticalValidations::validateInt($order); + } catch (Exception $e) { + return $e->getMessage(); + } + + $valueSet = self::rankFilterValues($valueSet); + if ($order === self::RANK_SORT_DESCENDING) { + rsort($valueSet, SORT_NUMERIC); + } else { + sort($valueSet, SORT_NUMERIC); + } + + $pos = array_search($value, $valueSet); + if ($pos === false) { + return ExcelError::NA(); + } + + return ++$pos; + } + + /** + * @param mixed[] $dataSet + * + * @return mixed[] + */ + protected static function percentileFilterValues(array $dataSet): array + { + return array_filter( + $dataSet, + fn ($value): bool => is_numeric($value) && !is_string($value) + ); + } + + /** + * @param mixed[] $dataSet + * + * @return mixed[] + */ + protected static function rankFilterValues(array $dataSet): array + { + return array_filter( + $dataSet, + fn ($value): bool => is_numeric($value) + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php new file mode 100644 index 0000000..4863e14 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -0,0 +1,100 @@ +|float|int|string Number of permutations, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function PERMUT(mixed $numObjs, mixed $numInSet) + { + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } + + try { + $numObjs = StatisticalValidations::validateInt($numObjs); + $numInSet = StatisticalValidations::validateInt($numInSet); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($numObjs < $numInSet) { + return ExcelError::NAN(); + } + /** @var float|int|string */ + $result1 = MathTrig\Factorial::fact($numObjs); + if (is_string($result1)) { + return $result1; + } + /** @var float|int|string */ + $result2 = MathTrig\Factorial::fact($numObjs - $numInSet); + if (is_string($result2)) { + return $result2; + } + $result = round($result1 / $result2); + + return IntOrFloat::evaluate($result); + } + + /** + * PERMUTATIONA. + * + * Returns the number of permutations for a given number of objects (with repetitions) + * that can be selected from the total objects. + * + * @param mixed $numObjs Integer number of different objects + * Or can be an array of values + * @param mixed $numInSet Integer number of objects in each permutation + * Or can be an array of values + * + * @return array|float|int|string Number of permutations, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function PERMUTATIONA(mixed $numObjs, mixed $numInSet) + { + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } + + try { + $numObjs = StatisticalValidations::validateInt($numObjs); + $numInSet = StatisticalValidations::validateInt($numInSet); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($numObjs < 0 || $numInSet < 0) { + return ExcelError::NAN(); + } + + $result = $numObjs ** $numInSet; + + return IntOrFloat::evaluate($result); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Size.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Size.php new file mode 100644 index 0000000..134aa66 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/Size.php @@ -0,0 +1,101 @@ += $count) { + return ExcelError::NAN(); + } + rsort($mArgs); + /** @var float[] $mArgs */ + + return $mArgs[$entry]; + } + + return ExcelError::VALUE(); + } + + /** + * SMALL. + * + * Returns the nth smallest value in a data set. You can use this function to + * select a value based on its relative standing. + * + * Excel Function: + * SMALL(value1[,value2[, ...]],entry) + * + * @param mixed $args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function small(mixed ...$args) + { + $aArgs = Functions::flattenArray($args); + + $entry = array_pop($aArgs); + + if ((is_numeric($entry)) && (!is_string($entry))) { + $entry = (int) floor($entry); + + $mArgs = self::filter($aArgs); + $count = Counts::COUNT($mArgs); + --$entry; + if ($count === 0 || $entry < 0 || $entry >= $count) { + return ExcelError::NAN(); + } + sort($mArgs); + /** @var float[] $mArgs */ + + return $mArgs[$entry]; + } + + return ExcelError::VALUE(); + } + + /** + * @param mixed[] $args Data values + * + * @return mixed[] + */ + protected static function filter(array $args): array + { + $mArgs = []; + + foreach ($args as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $mArgs[] = $arg; + } + } + + return $mArgs; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php new file mode 100644 index 0000000..3d4ea30 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php @@ -0,0 +1,89 @@ +|float $value Value to normalize + * Or can be an array of values + * @param array|float $mean Mean Value + * Or can be an array of values + * @param array|float $stdDev Standard Deviation + * Or can be an array of values + * + * @return array|float|string Standardized value, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function execute($value, $mean, $stdDev): array|string|float + { + if (is_array($value) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev); + } + + try { + $value = self::validateFloat($value); + $mean = self::validateFloat($mean); + $stdDev = self::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev <= 0) { + return ExcelError::NAN(); + } + + return ($value - $mean) / $stdDev; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php new file mode 100644 index 0000000..59ad7ee --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php @@ -0,0 +1,36 @@ + $array1 + * @param array $array2 + */ + private static function filterTrendValues(array &$array1, array &$array2): void + { + foreach ($array1 as $key => $value) { + if ((is_bool($value)) || (is_string($value)) || ($value === null)) { + unset($array1[$key], $array2[$key]); + } + } + } + + /** + * @param mixed $array1 should be array, but scalar is made into one + * @param mixed $array2 should be array, but scalar is made into one + * + * @param-out array $array1 + * @param-out array $array2 + */ + private static function checkTrendArrays(mixed &$array1, mixed &$array2): void + { + if (!is_array($array1)) { + $array1 = [$array1]; + } + if (!is_array($array2)) { + $array2 = [$array2]; + } + + $array1 = Functions::flattenArray($array1); + $array2 = Functions::flattenArray($array2); + + self::filterTrendValues($array1, $array2); + self::filterTrendValues($array2, $array1); + + // Reset the array indexes + $array1 = array_merge($array1); + $array2 = array_merge($array2); + } + + /** + * @param mixed[] $yValues + * @param mixed[] $xValues + */ + protected static function validateTrendArrays(array $yValues, array $xValues): void + { + $yValueCount = count($yValues); + $xValueCount = count($xValues); + + if (($yValueCount === 0) || ($yValueCount !== $xValueCount)) { + throw new Exception(ExcelError::NA()); + } elseif ($yValueCount === 1) { + throw new Exception(ExcelError::DIV0()); + } + } + + /** + * CORREL. + * + * Returns covariance, the average of the products of deviations for each data point pair. + * + * @param mixed $yValues array of mixed Data Series Y + * @param null|mixed $xValues array of mixed Data Series X + */ + public static function CORREL(mixed $yValues, $xValues = null): float|string + { + if (($xValues === null) || (!is_array($yValues)) || (!is_array($xValues))) { + return ExcelError::VALUE(); + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getCorrelation(); + } + + /** + * COVAR. + * + * Returns covariance, the average of the products of deviations for each data point pair. + * + * @param mixed[] $yValues array of mixed Data Series Y + * @param mixed[] $xValues array of mixed Data Series X + */ + public static function COVAR(array $yValues, array $xValues): float|string + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getCovariance(); + } + + /** + * FORECAST. + * + * Calculates, or predicts, a future value by using existing values. + * The predicted value is a y-value for a given x-value. + * + * @param mixed $xValue Float value of X for which we want to find Y + * Or can be an array of values + * @param mixed[] $yValues array of mixed Data Series Y + * @param mixed[] $xValues array of mixed Data Series X + * + * @return array|bool|float|string If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function FORECAST(mixed $xValue, array $yValues, array $xValues) + { + if (is_array($xValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $xValue, $yValues, $xValues); + } + + try { + $xValue = StatisticalValidations::validateFloat($xValue); + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getValueOfYForX($xValue); + } + + /** + * GROWTH. + * + * Returns values along a predicted exponential Trend + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * @param mixed[] $newValues Values of X for which we want to find Y + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not + * + * @return array>> + */ + public static function GROWTH(array $yValues, array $xValues = [], array $newValues = [], mixed $const = true): array + { + $yValues = Functions::flattenArray($yValues); + $xValues = Functions::flattenArray($xValues); + $newValues = Functions::flattenArray($newValues); + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + + $bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); + if (empty($newValues)) { + $newValues = $bestFitExponential->getXValues(); + } + + $returnArray = []; + foreach ($newValues as $xValue) { + /** @var float $xValue */ + $returnArray[0][] = [$bestFitExponential->getValueOfYForX($xValue)]; + } + + return $returnArray; + } + + /** + * INTERCEPT. + * + * Calculates the point at which a line will intersect the y-axis by using existing x-values and y-values. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + */ + public static function INTERCEPT(array $yValues, array $xValues): float|string + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getIntersect(); + } + + /** + * LINEST. + * + * Calculates the statistics for a line by using the "least squares" method to calculate a straight line + * that best fits your data, and then returns an array that describes the line. + * + * @param mixed[] $yValues Data Series Y + * @param null|mixed[] $xValues Data Series X + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not + * @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics + * + * @return array|string The result, or a string containing an error + */ + public static function LINEST(array $yValues, ?array $xValues = null, mixed $const = true, mixed $stats = false): string|array + { + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + $stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); + if ($xValues === null) { + $xValues = $yValues; + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); + + if ($stats === true) { + return [ + [ + $bestFitLinear->getSlope(), + $bestFitLinear->getIntersect(), + ], + [ + $bestFitLinear->getSlopeSE(), + ($const === false) ? ExcelError::NA() : $bestFitLinear->getIntersectSE(), + ], + [ + $bestFitLinear->getGoodnessOfFit(), + $bestFitLinear->getStdevOfResiduals(), + ], + [ + $bestFitLinear->getF(), + $bestFitLinear->getDFResiduals(), + ], + [ + $bestFitLinear->getSSRegression(), + $bestFitLinear->getSSResiduals(), + ], + ]; + } + + return [ + $bestFitLinear->getSlope(), + $bestFitLinear->getIntersect(), + ]; + } + + /** + * LOGEST. + * + * Calculates an exponential curve that best fits the X and Y data series, + * and then returns an array that describes the line. + * + * @param mixed[] $yValues Data Series Y + * @param null|mixed[] $xValues Data Series X + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not + * @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics + * + * @return array|string The result, or a string containing an error + */ + public static function LOGEST(array $yValues, ?array $xValues = null, mixed $const = true, mixed $stats = false): string|array + { + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + $stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); + if ($xValues === null) { + $xValues = $yValues; + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + foreach ($yValues as $value) { + if ($value < 0.0) { + return ExcelError::NAN(); + } + } + + $bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); + + if ($stats === true) { + return [ + [ + $bestFitExponential->getSlope(), + $bestFitExponential->getIntersect(), + ], + [ + $bestFitExponential->getSlopeSE(), + ($const === false) ? ExcelError::NA() : $bestFitExponential->getIntersectSE(), + ], + [ + $bestFitExponential->getGoodnessOfFit(), + $bestFitExponential->getStdevOfResiduals(), + ], + [ + $bestFitExponential->getF(), + $bestFitExponential->getDFResiduals(), + ], + [ + $bestFitExponential->getSSRegression(), + $bestFitExponential->getSSResiduals(), + ], + ]; + } + + return [ + $bestFitExponential->getSlope(), + $bestFitExponential->getIntersect(), + ]; + } + + /** + * RSQ. + * + * Returns the square of the Pearson product moment correlation coefficient through data points + * in known_y's and known_x's. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * + * @return float|string The result, or a string containing an error + */ + public static function RSQ(array $yValues, array $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getGoodnessOfFit(); + } + + /** + * SLOPE. + * + * Returns the slope of the linear regression line through data points in known_y's and known_x's. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * + * @return float|string The result, or a string containing an error + */ + public static function SLOPE(array $yValues, array $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getSlope(); + } + + /** + * STEYX. + * + * Returns the standard error of the predicted y-value for each x in the regression. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + */ + public static function STEYX(array $yValues, array $xValues): float|string + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getStdevOfResiduals(); + } + + /** + * TREND. + * + * Returns values along a linear Trend + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * @param mixed[] $newValues Values of X for which we want to find Y + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not + * + * @return array>> + */ + public static function TREND(array $yValues, array $xValues = [], array $newValues = [], mixed $const = true): array + { + $yValues = Functions::flattenArray($yValues); + $xValues = Functions::flattenArray($xValues); + $newValues = Functions::flattenArray($newValues); + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); + if (empty($newValues)) { + $newValues = $bestFitLinear->getXValues(); + } + + $returnArray = []; + foreach ($newValues as $xValue) { + /** @var float $xValue */ + $returnArray[0][] = [$bestFitLinear->getValueOfYForX($xValue)]; + } + + return $returnArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php new file mode 100644 index 0000000..d5646cf --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php @@ -0,0 +1,28 @@ + 1) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * ($aCount - 1)); + } + + return $returnValue; + } + + /** + * VARA. + * + * Estimates variance based on a sample, including numbers, text, and logical values + * + * Excel Function: + * VARA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function VARA(mixed ...$args): string|float + { + $returnValue = ExcelError::DIV0(); + + $summerA = $summerB = 0.0; + + // Loop through arguments + $aArgs = Functions::flattenArrayIndexed($args); + $aCount = 0; + foreach ($aArgs as $k => $arg) { + if ((is_string($arg)) && (Functions::isValue($k))) { + return ExcelError::VALUE(); + } elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + $summerA += ($arg * $arg); + $summerB += $arg; + ++$aCount; + } + } + } + + if ($aCount > 1) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * ($aCount - 1)); + } + + return $returnValue; + } + + /** + * VARP. + * + * Calculates variance based on the entire population + * + * Excel Function: + * VARP(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function VARP(mixed ...$args): float|string + { + // Return value + $returnValue = ExcelError::DIV0(); + + $summerA = $summerB = 0.0; + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + $aCount = 0; + foreach ($aArgs as $arg) { + $arg = self::datatypeAdjustmentBooleans($arg); + + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $summerA += ($arg * $arg); + $summerB += $arg; + ++$aCount; + } + } + + if ($aCount > 0) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * $aCount); + } + + return $returnValue; + } + + /** + * VARPA. + * + * Calculates variance based on the entire population, including numbers, text, and logical values + * + * Excel Function: + * VARPA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function VARPA(mixed ...$args): string|float + { + $returnValue = ExcelError::DIV0(); + + $summerA = $summerB = 0.0; + + // Loop through arguments + $aArgs = Functions::flattenArrayIndexed($args); + $aCount = 0; + foreach ($aArgs as $k => $arg) { + if ((is_string($arg)) && (Functions::isValue($k))) { + return ExcelError::VALUE(); + } elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + $summerA += ($arg * $arg); + $summerB += $arg; + ++$aCount; + } + } + } + + if ($aCount > 0) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * $aCount); + } + + return $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php new file mode 100644 index 0000000..ff67feb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -0,0 +1,90 @@ +|string If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function lower(mixed $mixedCaseValue): array|string + { + if (is_array($mixedCaseValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); + } + + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return StringHelper::strToLower($mixedCaseValue); + } + + /** + * UPPERCASE. + * + * Converts a string value to upper case. + * + * @param mixed $mixedCaseValue The string value to convert to upper case + * Or can be an array of values + * + * @return array|string If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function upper(mixed $mixedCaseValue): array|string + { + if (is_array($mixedCaseValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); + } + + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return StringHelper::strToUpper($mixedCaseValue); + } + + /** + * PROPERCASE. + * + * Converts a string value to proper or title case. + * + * @param mixed $mixedCaseValue The string value to convert to title case + * Or can be an array of values + * + * @return array|string If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function proper(mixed $mixedCaseValue): array|string + { + if (is_array($mixedCaseValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); + } + + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return StringHelper::strToTitle($mixedCaseValue); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php new file mode 100644 index 0000000..2f15bfc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php @@ -0,0 +1,93 @@ +|string The character string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function character(mixed $character): array|string + { + if (is_array($character)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character); + } + + try { + $character = Helpers::validateInt($character, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1; + if ($character < $min || $character > 255) { + return ExcelError::VALUE(); + } + $result = iconv('UCS-4LE', 'UTF-8', pack('V', $character)); + + return ($result === false) ? '' : $result; + } + + /** + * CODE. + * + * @param mixed $characters String character to convert to its ASCII value + * Or can be an array of values + * + * @return array|int|string A string if arguments are invalid + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function code(mixed $characters): array|string|int + { + if (is_array($characters)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters); + } + + try { + $characters = Helpers::extractString($characters, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + if ($characters === '') { + return ExcelError::VALUE(); + } + + $character = $characters; + if (mb_strlen($characters, 'UTF-8') > 1) { + $character = mb_substr($characters, 0, 1, 'UTF-8'); + } + + return self::unicodeToOrd($character); + } + + private static function unicodeToOrd(string $character): int + { + $retVal = 0; + $iconv = iconv('UTF-8', 'UCS-4LE', $character); + if ($iconv !== false) { + /** @var false|int[] */ + $result = unpack('V', $iconv); + if (is_array($result) && isset($result[1])) { + $retVal = $result[1]; + } + } + + return $retVal; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php new file mode 100644 index 0000000..d51d44b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -0,0 +1,207 @@ + DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + + break; + } + } + + return $returnValue; + } + + /** + * This implements the CONCATENATE function. + * + * @param mixed[] $args data to be concatenated + * + * @return array|string + */ + public static function actualCONCATENATE(...$args): array|string + { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) { + return self::CONCATENATE(...$args); + } + $result = ''; + foreach ($args as $operand2) { + $result = self::concatenate2Args($result, $operand2); + if (ErrorValue::isError($result, true) === true) { + break; + } + } + + return $result; + } + + /** + * @param array|string $operand1 + * @param null|array|bool|float|int|string $operand2 + * + * @return array|string + */ + private static function concatenate2Args(array|string $operand1, null|array|bool|float|int|string $operand2): array|string + { + if (is_array($operand1) || is_array($operand2)) { + $operand1 = Calculation::boolToString($operand1); + $operand2 = Calculation::boolToString($operand2); + [$rows, $columns] = Calculation::checkMatrixOperands($operand1, $operand2, 2); + $errorFound = false; + for ($row = 0; $row < $rows && !$errorFound; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + /** @var string[][] $operand2 */ + if (ErrorValue::isError($operand2[$row][$column])) { + return $operand2[$row][$column]; + } + /** @var string[][] $operand1 */ + $operand1[$row][$column] + = StringHelper::convertToString($operand1[$row][$column], convertBool: true) + . StringHelper::convertToString($operand2[$row][$column], convertBool: true); + if (mb_strlen($operand1[$row][$column]) > DataType::MAX_STRING_LENGTH) { + $operand1 = ExcelError::CALC(); + $errorFound = true; + + break; + } + } + } + } elseif (ErrorValue::isError($operand2, true) === true) { + $operand1 = (string) $operand2; + } else { + $operand1 .= StringHelper::convertToString($operand2, convertBool: true); + if (mb_strlen($operand1) > DataType::MAX_STRING_LENGTH) { + $operand1 = ExcelError::CALC(); + } + } + /** @var array|string $operand1 */ + + return $operand1; + } + + /** + * TEXTJOIN. + * + * @param null|string|string[] $delimiter The delimiter to use between the joined arguments + * Or can be an array of values + * @param null|bool|bool[] $ignoreEmpty true/false Flag indicating whether empty arguments should be skipped + * Or can be an array of values + * @param mixed $args The values to join + * + * @return array|string The joined string + * If an array of values is passed for the $delimiter or $ignoreEmpty arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function TEXTJOIN($delimiter = '', $ignoreEmpty = true, mixed ...$args): array|string + { + if (is_array($delimiter) || is_array($ignoreEmpty)) { + return self::evaluateArrayArgumentsSubset( + [self::class, __FUNCTION__], + 2, + $delimiter, + $ignoreEmpty, + ...$args + ); + } + + $delimiter ??= ''; + $ignoreEmpty ??= true; + /** @var mixed[] */ + $aArgs = Functions::flattenArray($args); + $returnValue = self::evaluateTextJoinArray($ignoreEmpty, $aArgs); + + $returnValue ??= implode($delimiter, $aArgs); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + } + + return $returnValue; + } + + /** @param mixed[] $aArgs */ + private static function evaluateTextJoinArray(bool $ignoreEmpty, array &$aArgs): ?string + { + foreach ($aArgs as $key => &$arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value, true)) { + return $value; + } + + if ($ignoreEmpty === true && ((is_string($arg) && trim($arg) === '') || $arg === null)) { + unset($aArgs[$key]); + } elseif (is_bool($arg)) { + $arg = Helpers::convertBooleanValue($arg); + } + } + + return null; + } + + /** + * REPT. + * + * Returns the result of builtin function round after validating args. + * + * @param mixed $stringValue The value to repeat + * Or can be an array of values + * @param mixed $repeatCount The number of times the string value should be repeated + * Or can be an array of values + * + * @return array|string The repeated string + * If an array of values is passed for the $stringValue or $repeatCount arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function builtinREPT(mixed $stringValue, mixed $repeatCount): array|string + { + if (is_array($stringValue) || is_array($repeatCount)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $stringValue, $repeatCount); + } + + $stringValue = Helpers::extractString($stringValue); + + if (!is_numeric($repeatCount) || $repeatCount < 0) { + $returnValue = ExcelError::VALUE(); + } elseif (ErrorValue::isError($stringValue, true)) { + $returnValue = $stringValue; + } else { + $returnValue = str_repeat($stringValue, (int) $repeatCount); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); // note VALUE not CALC + } + } + + return $returnValue; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Extract.php new file mode 100644 index 0000000..6a05fa0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -0,0 +1,290 @@ +|string The joined string + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function left(mixed $value, mixed $chars = 1): array|string + { + if (is_array($value) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars); + } + + try { + $value = Helpers::extractString($value, true); + $chars = Helpers::extractInt($chars, 0, 1); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return mb_substr($value, 0, $chars, 'UTF-8'); + } + + /** + * MID. + * + * @param mixed $value String value from which to extract characters + * Or can be an array of values + * @param mixed $start Integer offset of the first character that we want to extract + * Or can be an array of values + * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value, $start or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function mid(mixed $value, mixed $start, mixed $chars): array|string + { + if (is_array($value) || is_array($start) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $start, $chars); + } + + try { + $value = Helpers::extractString($value, true); + $start = Helpers::extractInt($start, 1); + $chars = Helpers::extractInt($chars, 0); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return mb_substr($value, --$start, $chars, 'UTF-8'); + } + + /** + * RIGHT. + * + * @param mixed $value String value from which to extract characters + * Or can be an array of values + * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function right(mixed $value, mixed $chars = 1): array|string + { + if (is_array($value) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars); + } + + try { + $value = Helpers::extractString($value, true); + $chars = Helpers::extractInt($chars, 0, 1); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return mb_substr($value, mb_strlen($value, 'UTF-8') - $chars, $chars, 'UTF-8'); + } + + /** + * TEXTBEFORE. + * + * @param mixed $text the text that you're searching + * Or can be an array of values + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param array|bool|float|int|string $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return array|string the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function before(mixed $text, $delimiter, mixed $instance = 1, mixed $matchMode = 0, mixed $matchEnd = 0, mixed $ifNotFound = '#N/A'): array|string + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + try { + $text = Helpers::extractString($text ?? '', true); + Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $instance = (int) StringHelper::convertToString($instance); + $matchMode = (int) StringHelper::convertToString($matchMode); + $matchEnd = (int) StringHelper::convertToString($matchEnd); + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_string($split)) { + return $split; + } + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { + return ($instance > 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, 0, max(count($split) - (abs($instance) * 2 - 1) - $adjust - $oddReverseAdjustment, 0)) + : array_slice($split, 0, $instance * 2 - 1 - $adjust); + + return implode('', $split); + } + + /** + * TEXTAFTER. + * + * @param mixed $text the text that you're searching + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param array|scalar $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return array|string the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function after(mixed $text, $delimiter, mixed $instance = 1, mixed $matchMode = 0, mixed $matchEnd = 0, mixed $ifNotFound = '#N/A'): array|string + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + try { + $text = Helpers::extractString($text ?? '', true); + Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $instance = (int) StringHelper::convertToString($instance); + $matchMode = (int) StringHelper::convertToString($matchMode); + $matchEnd = (int) StringHelper::convertToString($matchEnd); + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_string($split)) { + return $split; + } + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { + return ($instance < 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, count($split) - ((int) abs($instance + 1) * 2) - $adjust - $oddReverseAdjustment) + : array_slice($split, $instance * 2 - $adjust); + + return implode('', $split); + } + + /** + * @param null|array|string $delimiter + * @param array|scalar $ifNotFound + * + * @return array|string + */ + private static function validateTextBeforeAfter(string $text, null|array|string $delimiter, int $instance, int $matchMode, int $matchEnd, mixed $ifNotFound): array|string + { + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + + if (preg_match('/' . $delimiter . "/{$flags}", $text) === 0 && $matchEnd === 0) { + return is_array($ifNotFound) ? $ifNotFound : StringHelper::convertToString($ifNotFound); + } + + $split = preg_split('/' . $delimiter . "/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + if ($split === false) { + return ExcelError::NA(); + } + + if ($instance === 0 || abs($instance) > StringHelper::countCharacters($text)) { + return ExcelError::VALUE(); + } + + if ($matchEnd === 0 && (abs($instance) > floor(count($split) / 2))) { + return ExcelError::NA(); + } elseif ($matchEnd !== 0 && (abs($instance) - 1 > ceil(count($split) / 2))) { + return ExcelError::NA(); + } + + return $split; + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + if (is_array($delimiter)) { + /** @var array */ + $delimiter = Functions::flattenArray($delimiter); + $quotedDelimiters = array_map( + fn (?string $delimiter): string => preg_quote($delimiter ?? '', '/'), + $delimiter + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + /** @var ?string $delimiter */ + + return '(' . preg_quote($delimiter ?? '', '/') . ')'; + } + + private static function matchFlags(int $matchMode): string + { + return ($matchMode === 0) ? 'mu' : 'miu'; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Format.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Format.php new file mode 100644 index 0000000..23bf74f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -0,0 +1,328 @@ +|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function DOLLAR(mixed $value = 0, mixed $decimals = 2) + { + if (is_array($value) || is_array($decimals)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimals); + } + + try { + $value = Helpers::extractFloat($value); + $decimals = Helpers::extractInt($decimals, -100, 0, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $mask = '$#,##0'; + if ($decimals > 0) { + $mask .= '.' . str_repeat('0', $decimals); + } else { + $round = 10 ** abs($decimals); + if ($value < 0) { + $round = 0 - $round; + } + /** @var float|int|string */ + $value = MathTrig\Round::multiple($value, $round); + } + $mask = "{$mask};-{$mask}"; + + return NumberFormat::toFormattedString($value, $mask); + } + + /** + * FIXED. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $decimals Integer value for the number of decimal places that should be formatted + * Or can be an array of values + * @param mixed $noCommas Boolean value indicating whether the value should have thousands separators or not + * Or can be an array of values + * + * @return array|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function FIXEDFORMAT(mixed $value, mixed $decimals = 2, mixed $noCommas = false): array|string + { + if (is_array($value) || is_array($decimals) || is_array($noCommas)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimals, $noCommas); + } + + try { + $value = Helpers::extractFloat($value); + $decimals = Helpers::extractInt($decimals, -100, 0, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $valueResult = round($value, $decimals); + if ($decimals < 0) { + $decimals = 0; + } + if ($noCommas === false) { + $valueResult = number_format( + $valueResult, + $decimals, + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + } + + return (string) $valueResult; + } + + /** + * TEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $format A string with the Format mask that should be used + * Or can be an array of values + * + * @return array|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function TEXTFORMAT(mixed $value, mixed $format): array|string + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + try { + $value = Helpers::extractString($value, true); + $format = Helpers::extractString($format, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + $format = (string) NumberFormat::convertSystemFormats($format); + + if (!is_numeric($value) && Date::isDateTimeFormatCode($format) && !Preg::isMatch('/^\s*\d+(\s+\d+)+\s*$/', $value)) { + $value1 = DateTimeExcel\DateValue::fromString($value); + $value2 = DateTimeExcel\TimeValue::fromString($value); + /** @var float|int|string */ + $value = (is_numeric($value1) && is_numeric($value2)) ? ($value1 + $value2) : (is_numeric($value1) ? $value1 : (is_numeric($value2) ? $value2 : $value)); + } + + return (string) NumberFormat::toFormattedString($value, $format); + } + + /** + * @param mixed $value Value to check + */ + private static function convertValue(mixed $value, bool $spacesMeanZero = false): mixed + { + $value = $value ?? 0; + if (is_bool($value)) { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + $value = (int) $value; + } else { + throw new CalcExp(ExcelError::VALUE()); + } + } + if (is_string($value)) { + $value = trim($value); + if (ErrorValue::isError($value, true)) { + throw new CalcExp($value); + } + if ($spacesMeanZero && $value === '') { + $value = 0; + } + } + + return $value; + } + + /** + * VALUE. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|DateTimeInterface|float|int|string A string if arguments are invalid + * If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions + */ + public static function VALUE(mixed $value = '') + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = self::convertValue($value); + } catch (CalcExp $e) { + return $e->getMessage(); + } + if (!is_numeric($value)) { + $value = StringHelper::convertToString($value); + $numberValue = str_replace( + StringHelper::getThousandsSeparator(), + '', + trim($value, " \t\n\r\0\x0B" . StringHelper::getCurrencyCode()) + ); + if ($numberValue === '') { + return ExcelError::VALUE(); + } + if (is_numeric($numberValue)) { + return (float) $numberValue; + } + + $dateSetting = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + + if (str_contains($value, ':')) { + $timeValue = Functions::scalar(DateTimeExcel\TimeValue::fromString($value)); + if ($timeValue !== ExcelError::VALUE()) { + Functions::setReturnDateType($dateSetting); + + return $timeValue; //* @phpstan-ignore-line + } + } + $dateValue = Functions::scalar(DateTimeExcel\DateValue::fromString($value)); + if ($dateValue !== ExcelError::VALUE()) { + Functions::setReturnDateType($dateSetting); + + return $dateValue; //* @phpstan-ignore-line + } + Functions::setReturnDateType($dateSetting); + + return ExcelError::VALUE(); + } + + return (float) $value; + } + + /** + * VALUETOTEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * + * @return array|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function valueToText(mixed $value, mixed $format = false): array|string + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + $format = (bool) $format; + + if (is_object($value) && $value instanceof RichText) { + $value = $value->getPlainText(); + } + if (is_string($value)) { + $value = ($format === true) ? StringHelper::convertToString(Calculation::wrapResult($value)) : $value; + $value = str_replace("\n", '', $value); + } elseif (is_bool($value)) { + $value = Calculation::getLocaleBoolean($value ? 'TRUE' : 'FALSE'); + } + + return StringHelper::convertToString($value); + } + + private static function getDecimalSeparator(mixed $decimalSeparator): string + { + return empty($decimalSeparator) ? StringHelper::getDecimalSeparator() : StringHelper::convertToString($decimalSeparator); + } + + private static function getGroupSeparator(mixed $groupSeparator): string + { + return empty($groupSeparator) ? StringHelper::getThousandsSeparator() : StringHelper::convertToString($groupSeparator); + } + + /** + * NUMBERVALUE. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $decimalSeparator A string with the decimal separator to use, defaults to locale defined value + * Or can be an array of values + * @param mixed $groupSeparator A string with the group/thousands separator to use, defaults to locale defined value + * Or can be an array of values + * + * @return array|float|string + */ + public static function NUMBERVALUE(mixed $value = '', mixed $decimalSeparator = null, mixed $groupSeparator = null): array|string|float + { + if (is_array($value) || is_array($decimalSeparator) || is_array($groupSeparator)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimalSeparator, $groupSeparator); + } + + try { + $value = self::convertValue($value, true); + $decimalSeparator = self::getDecimalSeparator($decimalSeparator); + $groupSeparator = self::getGroupSeparator($groupSeparator); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + /** @var null|array|scalar $value */ + if (!is_array($value) && !is_numeric($value)) { + $value = StringHelper::convertToString($value); + $decimalPositions = Preg::matchAllWithOffsets('/' . preg_quote($decimalSeparator, '/') . '/', $value, $matches); + if ($decimalPositions > 1) { + return ExcelError::VALUE(); + } + $decimalOffset = array_pop($matches[0])[1] ?? null; + if ($decimalOffset === null || strpos($value, $groupSeparator, $decimalOffset) !== false) { + return ExcelError::VALUE(); + } + + $value = str_replace([$groupSeparator, $decimalSeparator], ['', '.'], $value); + + // Handle the special case of trailing % signs + $percentageString = rtrim($value, '%'); + if (!is_numeric($percentageString)) { + return ExcelError::VALUE(); + } + + $percentageAdjustment = strlen($value) - strlen($percentageString); + if ($percentageAdjustment) { + $value = (float) $percentageString; + $value /= 10 ** ($percentageAdjustment * 2); + } + } + + return is_array($value) ? ExcelError::VALUE() : (float) $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Helpers.php new file mode 100644 index 0000000..e6dad29 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -0,0 +1,93 @@ +|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function replace(mixed $oldText, mixed $start, mixed $chars, mixed $newText): array|string + { + if (is_array($oldText) || is_array($start) || is_array($chars) || is_array($newText)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $oldText, $start, $chars, $newText); + } + + try { + $start = Helpers::extractInt($start, 1, 0, true); + $chars = Helpers::extractInt($chars, 0, 0, true); + $oldText = Helpers::extractString($oldText, true); + $newText = Helpers::extractString($newText, true); + $left = StringHelper::substring($oldText, 0, $start - 1); + + $right = StringHelper::substring($oldText, $start + $chars - 1, null); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $returnValue = $left . $newText . $right; + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } + + return $returnValue; + } + + /** + * SUBSTITUTE. + * + * @param mixed $text The text string value to modify + * Or can be an array of values + * @param mixed $fromText The string value that we want to replace in $text + * Or can be an array of values + * @param mixed $toText The string value that we want to replace with in $text + * Or can be an array of values + * @param mixed $instance Integer instance Number for the occurrence of frmText to change + * Or can be an array of values + * + * @return array|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function substitute(mixed $text = '', mixed $fromText = '', mixed $toText = '', mixed $instance = null): array|string + { + if (is_array($text) || is_array($fromText) || is_array($toText) || is_array($instance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $text, $fromText, $toText, $instance); + } + + try { + $text = Helpers::extractString($text, true); + $fromText = Helpers::extractString($fromText, true); + $toText = Helpers::extractString($toText, true); + if ($instance === null) { + $returnValue = str_replace($fromText, $toText, $text); + } else { + if (is_bool($instance)) { + if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { + return ExcelError::Value(); + } + $instance = 1; + } + $instance = Helpers::extractInt($instance, 1, 0, true); + $returnValue = self::executeSubstitution($text, $fromText, $toText, $instance); + } + } catch (CalcExp $e) { + return $e->getMessage(); + } + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } + + return $returnValue; + } + + private static function executeSubstitution(string $text, string $fromText, string $toText, int $instance): string + { + $pos = -1; + while ($instance > 0) { + $pos = mb_strpos($text, $fromText, $pos + 1, 'UTF-8'); + if ($pos === false) { + return $text; + } + --$instance; + } + + return StringHelper::convertToString(Functions::scalar(self::REPLACE($text, ++$pos, StringHelper::countCharacters($fromText), $toText))); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Search.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Search.php new file mode 100644 index 0000000..0aad5ed --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -0,0 +1,97 @@ +|int|string The offset where the first occurrence of needle was found in the haystack + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function sensitive(mixed $needle, mixed $haystack, mixed $offset = 1): array|string|int + { + if (is_array($needle) || is_array($haystack) || is_array($offset)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $needle, $haystack, $offset); + } + + try { + $needle = Helpers::extractString($needle, true); + $haystack = Helpers::extractString($haystack, true); + $offset = Helpers::extractInt($offset, 1, 0, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + if (StringHelper::countCharacters($haystack) >= $offset) { + if (StringHelper::countCharacters($needle) === 0) { + return $offset; + } + + $pos = mb_strpos($haystack, $needle, --$offset, 'UTF-8'); + if ($pos !== false) { + return ++$pos; + } + } + + return ExcelError::VALUE(); + } + + /** + * SEARCH (case-insensitive search). + * + * @param mixed $needle The string to look for + * Or can be an array of values + * @param mixed $haystack The string in which to look + * Or can be an array of values + * @param mixed $offset Integer offset within $haystack to start searching from + * Or can be an array of values + * + * @return array|int|string The offset where the first occurrence of needle was found in the haystack + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function insensitive(mixed $needle, mixed $haystack, mixed $offset = 1): array|string|int + { + if (is_array($needle) || is_array($haystack) || is_array($offset)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $needle, $haystack, $offset); + } + + try { + $needle = Helpers::extractString($needle, true); + $haystack = Helpers::extractString($haystack, true); + $offset = Helpers::extractInt($offset, 1, 0, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + if (StringHelper::countCharacters($haystack) >= $offset) { + if (StringHelper::countCharacters($needle) === 0) { + return $offset; + } + + $pos = mb_stripos($haystack, $needle, --$offset, 'UTF-8'); + if ($pos !== false) { + return ++$pos; + } + } + + return ExcelError::VALUE(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Text.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Text.php new file mode 100644 index 0000000..a4cc14f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -0,0 +1,250 @@ +|int|string If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions + */ + public static function length(mixed $value = ''): array|int|string + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + try { + $value = Helpers::extractString($value, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return mb_strlen($value, 'UTF-8'); + } + + /** + * Compares two text strings and returns TRUE if they are exactly the same, FALSE otherwise. + * EXACT is case-sensitive but ignores formatting differences. + * Use EXACT to test text being entered into a document. + * + * @param mixed $value1 String Value + * Or can be an array of values + * @param mixed $value2 String Value + * Or can be an array of values + * + * @return array|bool|string If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function exact(mixed $value1, mixed $value2): array|bool|string + { + if (is_array($value1) || is_array($value2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value1, $value2); + } + + try { + $value1 = Helpers::extractString($value1, true); + $value2 = Helpers::extractString($value2, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + + return $value2 === $value1; + } + + /** + * T. + * + * @param mixed $testValue Value to check + * Or can be an array of values + * + * @return array|string If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions + */ + public static function test(mixed $testValue = ''): array|string + { + if (is_array($testValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $testValue); + } + + if (is_string($testValue)) { + return $testValue; + } + + return ''; + } + + /** + * TEXTSPLIT. + * + * @param mixed $text the text that you're searching + * @param null|array|string $columnDelimiter The text that marks the point where to spill the text across columns. + * Multiple delimiters can be passed as an array of string values + * @param null|array|string $rowDelimiter The text that marks the point where to spill the text down rows. + * Multiple delimiters can be passed as an array of string values + * @param bool $ignoreEmpty Specify FALSE to create an empty cell when two delimiters are consecutive. + * true = create empty cells + * false = skip empty cells + * Defaults to TRUE, which creates an empty cell + * @param bool $matchMode Determines whether the match is case-sensitive or not. + * true = case-sensitive + * false = case-insensitive + * By default, a case-sensitive match is done. + * @param mixed $padding The value with which to pad the result. + * The default is #N/A. + * + * @return array|string the array built from the text, split by the row and column delimiters, or an error string + */ + public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array|string + { + $text = Functions::flattenSingleValue($text); + if (ErrorValue::isError($text, true)) { + return StringHelper::convertToString($text); + } + + $flags = self::matchFlags($matchMode); + + if ($rowDelimiter !== null) { + $delimiter = self::buildDelimiter($rowDelimiter); + $rows = ($delimiter === '()') + ? [$text] + : Preg::split("/{$delimiter}/{$flags}", StringHelper::convertToString($text)); + } else { + $rows = [$text]; + } + + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + fn ($row): bool => $row !== '' + )); + } + + if ($columnDelimiter !== null) { + $delimiter = self::buildDelimiter($columnDelimiter); + array_walk( + $rows, + function (&$row) use ($delimiter, $flags, $ignoreEmpty): void { + $row = ($delimiter === '()') + ? [$row] + : Preg::split("/{$delimiter}/{$flags}", StringHelper::convertToString($row)); + if ($ignoreEmpty === true) { + $row = array_values(array_filter( + $row, + fn ($value): bool => $value !== '' + )); + } + } + ); + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + fn ($row): bool => $row !== [] && $row !== [''] + )); + } + } + + return self::applyPadding($rows, $padding); + } + + /** + * @param mixed[] $rows + * + * @return mixed[] + */ + private static function applyPadding(array $rows, mixed $padding): array + { + $columnCount = array_reduce( + $rows, + fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore-line + 0 + ); + + return array_map( + fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore-line + ? array_merge($row, array_fill(0, $columnCount - count($row), $padding)) + : $row, + $rows + ); + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to split + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + $valueSet = Functions::flattenArray($delimiter); + + if (is_array($delimiter) && count($valueSet) > 1) { + /** @var array $valueSet */ + $quotedDelimiters = array_map( + fn (?string $delimiter): string => preg_quote($delimiter ?? '', '/'), + $valueSet + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote(StringHelper::convertToString(Functions::flattenSingleValue($delimiter)), '/') . ')'; + } + + private static function matchFlags(bool $matchMode): string + { + return ($matchMode === true) ? 'miu' : 'mu'; + } + + /** @param mixed[][] $array */ + public static function fromArray(array $array, int $format = 0): string + { + $result = []; + foreach ($array as $row) { + $cells = []; + foreach ($row as $cellValue) { + $value = ($format === 1) ? self::formatValueMode1($cellValue) : self::formatValueMode0($cellValue); + $cells[] = $value; + } + $result[] = implode(($format === 1) ? ',' : ', ', $cells); + } + + $result = implode(($format === 1) ? ';' : ', ', $result); + + return ($format === 1) ? '{' . $result . '}' : $result; + } + + private static function formatValueMode0(mixed $cellValue): string + { + if (is_bool($cellValue)) { + return Calculation::getLocaleBoolean($cellValue ? 'TRUE' : 'FALSE'); + } + + return StringHelper::convertToString($cellValue); + } + + private static function formatValueMode1(mixed $cellValue): string + { + if (is_string($cellValue) && ErrorValue::isError($cellValue) === false) { + return Calculation::FORMULA_STRING_QUOTE . $cellValue . Calculation::FORMULA_STRING_QUOTE; + } elseif (is_bool($cellValue)) { + return Calculation::getLocaleBoolean($cellValue ? 'TRUE' : 'FALSE'); + } + + return StringHelper::convertToString($cellValue); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Thai.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Thai.php new file mode 100644 index 0000000..0d08db0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Thai.php @@ -0,0 +1,139 @@ + 'ศูนย์', + 1 => 'หนึ่ง', + 2 => 'สอง', + 3 => 'สาม', + 4 => 'สี่', + 5 => 'ห้า', + 6 => 'หก', + 7 => 'เจ็ด', + 8 => 'แปด', + 9 => 'เก้า', + ]; + + private const THAI_UNITS = [ + 1 => 'สิบ', + 2 => 'ร้อย', + 3 => 'พัน', + 4 => 'หมื่น', + 5 => 'แสน', + 6 => 'ล้าน', + ]; + + private const THAI_COMPOUND_ONE = 'เอ็ด'; + private const THAI_COMPOUND_TWO = 'ยี่'; + private const THAI_INTEGER = 'ถ้วน'; + private const THAI_MINUS = 'ลบ'; + private const THAI_BAHT = 'บาท'; + private const THAI_SATANG = 'สตางค์'; + + /** + * BAHTTEXT. + * + * @param mixed $number The number or array of numbers to convert + * + * @return array|string If an array of values is passed as the argument, then the returned result will also be an array with the same dimensions + */ + public static function getBahtText(mixed $number): array|string + { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + + if (is_string($number) && preg_match('/^-?\d+$/', $number)) { + $isNegative = str_starts_with($number, '-'); + $baht = ltrim($number, '-0') ?: '0'; + $satang = '00'; + } elseif (is_bool($number) || is_numeric($number)) { + $number += 0; + $isNegative = $number < 0; + [$baht, $satang] = explode('.', number_format(abs($number), 2, '.', '')); + } else { + return ExcelError::VALUE(); + } + + $hasWhole = $baht !== '0'; + $hasFraction = $satang !== '00'; + + if (!$hasWhole && !$hasFraction) { + return self::THAI_DIGITS[0] . self::THAI_BAHT . self::THAI_INTEGER; + } + + $text = $isNegative + ? self::THAI_MINUS + : ''; + + if ($hasWhole) { + $text .= self::convertLarge($baht) . self::THAI_BAHT; + } + + $text .= $hasFraction + ? self::convertBlock($satang) . self::THAI_SATANG + : self::THAI_INTEGER; + + return $text; + } + + private static function convertLarge(string $digits): string + { + $length = strlen($digits) % 6 ?: 6; + + $chunks = [ + substr($digits, 0, $length), + ...str_split(substr($digits, $length), 6), + ]; + + $chunks = array_filter($chunks, fn (string $chunk): bool => $chunk !== ''); + + return implode( + self::THAI_UNITS[6], + array_map(self::convertBlock(...), $chunks) + ); + } + + private static function convertBlock(string $block): string + { + $out = ''; + $length = strlen($block); + $i = 0; + + // Hundreds and higher powers + for ($power = $length - 1; $power >= 2; --$power) { + $digit = $block[$i++]; + if ($digit !== '0') { + $out .= self::THAI_DIGITS[$digit] . self::THAI_UNITS[$power]; + } + } + + // Tens + $ten = $length > 1 ? $block[$i++] : '0'; + if ($ten !== '0') { + $out .= match ($ten) { + '1' => '', + '2' => self::THAI_COMPOUND_TWO, + default => self::THAI_DIGITS[$ten], + } . self::THAI_UNITS[1]; + } + + // Ones + $one = $block[$i] ?? '0'; + if ($one !== '0') { + $out .= $ten !== '0' && $one === '1' + ? self::THAI_COMPOUND_ONE + : self::THAI_DIGITS[$one]; + } + + return $out; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Trim.php new file mode 100644 index 0000000..0a7bb25 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -0,0 +1,50 @@ +|string If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function nonPrintable(mixed $stringValue = '') + { + if (is_array($stringValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); + } + + $stringValue = Helpers::extractString($stringValue); + + return (string) preg_replace('/[\x00-\x1f]/', '', "$stringValue"); + } + + /** + * TRIM. + * + * @param mixed $stringValue String Value to check + * Or can be an array of values + * + * @return array|string If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions + */ + public static function spaces(mixed $stringValue = ''): array|string + { + if (is_array($stringValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); + } + + $stringValue = Helpers::extractString($stringValue); + + return trim(preg_replace('/ +/', ' ', trim("$stringValue", ' ')) ?? '', ' '); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Token/Stack.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Token/Stack.php new file mode 100644 index 0000000..3301333 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Token/Stack.php @@ -0,0 +1,126 @@ +> + */ + private array $stack = []; + + /** + * Count of entries in the parser stack. + */ + private int $count = 0; + + public function __construct(BranchPruner $branchPruner) + { + $this->branchPruner = $branchPruner; + } + + /** + * Return the number of entries on the stack. + */ + public function count(): int + { + return $this->count; + } + + /** + * Push a new entry onto the stack. + */ + public function push(string $type, mixed $value, ?string $reference = null): void + { + $stackItem = $this->getStackItem($type, $value, $reference); + $this->stack[$this->count++] = $stackItem; + + if ($type === 'Function') { + $localeFunction = Calculation::localeFunc(StringHelper::convertToString($value)); + if ($localeFunction != $value) { + $this->stack[($this->count - 1)]['localeValue'] = $localeFunction; + } + } + } + + /** @param array $stackItem */ + public function pushStackItem(array $stackItem): void + { + $this->stack[$this->count++] = $stackItem; + } + + /** @return array */ + public function getStackItem(string $type, mixed $value, ?string $reference = null): array + { + $stackItem = [ + 'type' => $type, + 'value' => $value, + 'reference' => $reference, + ]; + + // will store the result under this alias + $storeKey = $this->branchPruner->currentCondition(); + if (isset($storeKey) || $reference === 'NULL') { + $stackItem['storeKey'] = $storeKey; + } + + // will only run computation if the matching store key is true + $onlyIf = $this->branchPruner->currentOnlyIf(); + if (isset($onlyIf) || $reference === 'NULL') { + $stackItem['onlyIf'] = $onlyIf; + } + + // will only run computation if the matching store key is false + $onlyIfNot = $this->branchPruner->currentOnlyIfNot(); + if (isset($onlyIfNot) || $reference === 'NULL') { + $stackItem['onlyIfNot'] = $onlyIfNot; + } + + return $stackItem; + } + + /** + * Pop the last entry from the stack. + * + * @return null|array + */ + public function pop(): ?array + { + if ($this->count > 0) { + return $this->stack[--$this->count]; + } + + return null; + } + + /** + * Return an entry from the stack without removing it. + * + * @return null|array + */ + public function last(int $n = 1): ?array + { + if ($this->count - $n < 0) { + return null; + } + + return $this->stack[$this->count - $n]; + } + + /** + * Clear the stack. + */ + public function clear(): void + { + $this->stack = []; + $this->count = 0; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Web/Service.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Web/Service.php new file mode 100644 index 0000000..ad06d39 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/Web/Service.php @@ -0,0 +1,73 @@ + 2048) { + return ExcelError::VALUE(); // Invalid URL length + } + + if (!preg_match('/^http[s]?:\/\//', $url)) { + return ExcelError::VALUE(); // Invalid protocol + } + + // Get results from the webservice + $client = Settings::getHttpClient(); + $requestFactory = Settings::getRequestFactory(); + $request = $requestFactory->createRequest('GET', $url); + + try { + $response = $client->sendRequest($request); + } catch (ClientExceptionInterface) { + return ExcelError::VALUE(); // cURL error + } + + if ($response->getStatusCode() != 200) { + return ExcelError::VALUE(); // cURL error + } + + $output = $response->getBody()->getContents(); + if (strlen($output) > 32767) { + return ExcelError::VALUE(); // Output not a string or too long + } + + return $output; + } + + /** + * URLENCODE. + * + * Returns data from a web service on the Internet or Intranet. + * + * Excel Function: + * urlEncode(text) + * + * @return string the url encoded output + */ + public static function urlEncode(mixed $text): string + { + if (!is_string($text)) { + return ExcelError::VALUE(); + } + + return str_replace('+', '%20', urlencode($text)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx new file mode 100644 index 0000000..78a67d0 Binary files /dev/null and b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx differ diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/bg/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/bg/config new file mode 100644 index 0000000..689203c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/bg/config @@ -0,0 +1,24 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## български (Bulgarian) +## +############################################################ + +ArgumentSeparator = ; +## +## (For future use) +## +currencySymbol = лв + +## +## Error Codes +## +NULL = #ПРАЗНО! +DIV0 = #ДЕЛ/0! +VALUE = #СТОЙНОСТ! +REF = #РЕФ! +NAME = #ИМЕ? +NUM = #ЧИСЛО! +NA = #Н/Д diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/bg/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/bg/functions new file mode 100644 index 0000000..ec56433 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/bg/functions @@ -0,0 +1,409 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## български (Bulgarian) +## +############################################################ + + +## +## Функции Куб (Cube Functions) +## +CUBEKPIMEMBER = КУБЭЛЕМЕНТКИП +CUBEMEMBER = КУБЭЛЕМЕНТ +CUBEMEMBERPROPERTY = КУБСВОЙСТВОЭЛЕМЕНТА +CUBERANKEDMEMBER = КУБПОРЭЛЕМЕНТ +CUBESET = КУБМНОЖ +CUBESETCOUNT = КУБЧИСЛОЭЛМНОЖ +CUBEVALUE = КУБЗНАЧЕНИЕ + +## +## Функции для работы с базами данных (Database Functions) +## +DAVERAGE = ДСРЗНАЧ +DCOUNT = БСЧЁТ +DCOUNTA = БСЧЁТА +DGET = БИЗВЛЕЧЬ +DMAX = ДМАКС +DMIN = ДМИН +DPRODUCT = БДПРОИЗВЕД +DSTDEV = ДСТАНДОТКЛ +DSTDEVP = ДСТАНДОТКЛП +DSUM = БДСУММ +DVAR = БДДИСП +DVARP = БДДИСПП + +## +## Функции даты и времени (Date & Time Functions) +## +DATE = ДАТА +DATEVALUE = ДАТАЗНАЧ +DAY = ДЕНЬ +DAYS360 = ДНЕЙ360 +EDATE = ДАТАМЕС +EOMONTH = КОНМЕСЯЦА +HOUR = ЧАС +MINUTE = МИНУТЫ +MONTH = МЕСЯЦ +NETWORKDAYS = ЧИСТРАБДНИ +NOW = ТДАТА +SECOND = СЕКУНДЫ +TIME = ВРЕМЯ +TIMEVALUE = ВРЕМЗНАЧ +TODAY = СЕГОДНЯ +WEEKDAY = ДЕНЬНЕД +WEEKNUM = НОМНЕДЕЛИ +WORKDAY = РАБДЕНЬ +YEAR = ГОД +YEARFRAC = ДОЛЯГОДА + +## +## Инженерные функции (Engineering Functions) +## +BESSELI = БЕССЕЛЬ.I +BESSELJ = БЕССЕЛЬ.J +BESSELK = БЕССЕЛЬ.K +BESSELY = БЕССЕЛЬ.Y +BIN2DEC = ДВ.В.ДЕС +BIN2HEX = ДВ.В.ШЕСТН +BIN2OCT = ДВ.В.ВОСЬМ +COMPLEX = КОМПЛЕКСН +CONVERT = ПРЕОБР +DEC2BIN = ДЕС.В.ДВ +DEC2HEX = ДЕС.В.ШЕСТН +DEC2OCT = ДЕС.В.ВОСЬМ +DELTA = ДЕЛЬТА +ERF = ФОШ +ERFC = ДФОШ +GESTEP = ПОРОГ +HEX2BIN = ШЕСТН.В.ДВ +HEX2DEC = ШЕСТН.В.ДЕС +HEX2OCT = ШЕСТН.В.ВОСЬМ +IMABS = МНИМ.ABS +IMAGINARY = МНИМ.ЧАСТЬ +IMARGUMENT = МНИМ.АРГУМЕНТ +IMCONJUGATE = МНИМ.СОПРЯЖ +IMCOS = МНИМ.COS +IMDIV = МНИМ.ДЕЛ +IMEXP = МНИМ.EXP +IMLN = МНИМ.LN +IMLOG10 = МНИМ.LOG10 +IMLOG2 = МНИМ.LOG2 +IMPOWER = МНИМ.СТЕПЕНЬ +IMPRODUCT = МНИМ.ПРОИЗВЕД +IMREAL = МНИМ.ВЕЩ +IMSIN = МНИМ.SIN +IMSQRT = МНИМ.КОРЕНЬ +IMSUB = МНИМ.РАЗН +IMSUM = МНИМ.СУММ +OCT2BIN = ВОСЬМ.В.ДВ +OCT2DEC = ВОСЬМ.В.ДЕС +OCT2HEX = ВОСЬМ.В.ШЕСТН + +## +## Финансовые функции (Financial Functions) +## +ACCRINT = НАКОПДОХОД +ACCRINTM = НАКОПДОХОДПОГАШ +AMORDEGRC = АМОРУМ +AMORLINC = АМОРУВ +COUPDAYBS = ДНЕЙКУПОНДО +COUPDAYS = ДНЕЙКУПОН +COUPDAYSNC = ДНЕЙКУПОНПОСЛЕ +COUPNCD = ДАТАКУПОНПОСЛЕ +COUPNUM = ЧИСЛКУПОН +COUPPCD = ДАТАКУПОНДО +CUMIPMT = ОБЩПЛАТ +CUMPRINC = ОБЩДОХОД +DB = ФУО +DDB = ДДОБ +DISC = СКИДКА +DOLLARDE = РУБЛЬ.ДЕС +DOLLARFR = РУБЛЬ.ДРОБЬ +DURATION = ДЛИТ +EFFECT = ЭФФЕКТ +FV = БС +FVSCHEDULE = БЗРАСПИС +INTRATE = ИНОРМА +IPMT = ПРПЛТ +IRR = ВСД +ISPMT = ПРОЦПЛАТ +MDURATION = МДЛИТ +MIRR = МВСД +NOMINAL = НОМИНАЛ +NPER = КПЕР +NPV = ЧПС +ODDFPRICE = ЦЕНАПЕРВНЕРЕГ +ODDFYIELD = ДОХОДПЕРВНЕРЕГ +ODDLPRICE = ЦЕНАПОСЛНЕРЕГ +ODDLYIELD = ДОХОДПОСЛНЕРЕГ +PMT = ПЛТ +PPMT = ОСПЛТ +PRICE = ЦЕНА +PRICEDISC = ЦЕНАСКИДКА +PRICEMAT = ЦЕНАПОГАШ +PV = ПС +RATE = СТАВКА +RECEIVED = ПОЛУЧЕНО +SLN = АПЛ +SYD = АСЧ +TBILLEQ = РАВНОКЧЕК +TBILLPRICE = ЦЕНАКЧЕК +TBILLYIELD = ДОХОДКЧЕК +VDB = ПУО +XIRR = ЧИСТВНДОХ +XNPV = ЧИСТНЗ +YIELD = ДОХОД +YIELDDISC = ДОХОДСКИДКА +YIELDMAT = ДОХОДПОГАШ + +## +## Информационные функции (Information Functions) +## +CELL = ЯЧЕЙКА +ERROR.TYPE = ТИП.ОШИБКИ +INFO = ИНФОРМ +ISBLANK = ЕПУСТО +ISERR = ЕОШ +ISERROR = ЕОШИБКА +ISEVEN = ЕЧЁТН +ISLOGICAL = ЕЛОГИЧ +ISNA = ЕНД +ISNONTEXT = ЕНЕТЕКСТ +ISNUMBER = ЕЧИСЛО +ISODD = ЕНЕЧЁТ +ISREF = ЕССЫЛКА +ISTEXT = ЕТЕКСТ +N = Ч +NA = НД +TYPE = ТИП + +## +## Логические функции (Logical Functions) +## +AND = И +FALSE = ЛОЖЬ +IF = ЕСЛИ +IFERROR = ЕСЛИОШИБКА +NOT = НЕ +OR = ИЛИ +TRUE = ИСТИНА + +## +## Функции ссылки и поиска (Lookup & Reference Functions) +## +ADDRESS = АДРЕС +AREAS = ОБЛАСТИ +CHOOSE = ВЫБОР +COLUMN = СТОЛБЕЦ +COLUMNS = ЧИСЛСТОЛБ +GETPIVOTDATA = ПОЛУЧИТЬ.ДАННЫЕ.СВОДНОЙ.ТАБЛИЦЫ +HLOOKUP = ГПР +HYPERLINK = ГИПЕРССЫЛКА +INDEX = ИНДЕКС +INDIRECT = ДВССЫЛ +LOOKUP = ПРОСМОТР +MATCH = ПОИСКПОЗ +OFFSET = СМЕЩ +ROW = СТРОКА +ROWS = ЧСТРОК +RTD = ДРВ +TRANSPOSE = ТРАНСП +VLOOKUP = ВПР + +## +## Математические и тригонометрические функции (Math & Trig Functions) +## +ABS = ABS +ACOS = ACOS +ACOSH = ACOSH +ASIN = ASIN +ASINH = ASINH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +COMBIN = ЧИСЛКОМБ +COS = COS +COSH = COSH +DEGREES = ГРАДУСЫ +EVEN = ЧЁТН +EXP = EXP +FACT = ФАКТР +FACTDOUBLE = ДВФАКТР +GCD = НОД +INT = ЦЕЛОЕ +LCM = НОК +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = МОПРЕД +MINVERSE = МОБР +MMULT = МУМНОЖ +MOD = ОСТАТ +MROUND = ОКРУГЛТ +MULTINOMIAL = МУЛЬТИНОМ +ODD = НЕЧЁТ +PI = ПИ +POWER = СТЕПЕНЬ +PRODUCT = ПРОИЗВЕД +QUOTIENT = ЧАСТНОЕ +RADIANS = РАДИАНЫ +RAND = СЛЧИС +RANDBETWEEN = СЛУЧМЕЖДУ +ROMAN = РИМСКОЕ +ROUND = ОКРУГЛ +ROUNDDOWN = ОКРУГЛВНИЗ +ROUNDUP = ОКРУГЛВВЕРХ +SERIESSUM = РЯД.СУММ +SIGN = ЗНАК +SIN = SIN +SINH = SINH +SQRT = КОРЕНЬ +SQRTPI = КОРЕНЬПИ +SUBTOTAL = ПРОМЕЖУТОЧНЫЕ.ИТОГИ +SUM = СУММ +SUMIF = СУММЕСЛИ +SUMIFS = СУММЕСЛИМН +SUMPRODUCT = СУММПРОИЗВ +SUMSQ = СУММКВ +SUMX2MY2 = СУММРАЗНКВ +SUMX2PY2 = СУММСУММКВ +SUMXMY2 = СУММКВРАЗН +TAN = TAN +TANH = TANH +TRUNC = ОТБР + +## +## Статистические функции (Statistical Functions) +## +AVEDEV = СРОТКЛ +AVERAGE = СРЗНАЧ +AVERAGEA = СРЗНАЧА +AVERAGEIF = СРЗНАЧЕСЛИ +AVERAGEIFS = СРЗНАЧЕСЛИМН +CORREL = КОРРЕЛ +COUNT = СЧЁТ +COUNTA = СЧЁТЗ +COUNTBLANK = СЧИТАТЬПУСТОТЫ +COUNTIF = СЧЁТЕСЛИ +COUNTIFS = СЧЁТЕСЛИМН +DEVSQ = КВАДРОТКЛ +FISHER = ФИШЕР +FISHERINV = ФИШЕРОБР +FREQUENCY = ЧАСТОТА +GAMMALN = ГАММАНЛОГ +GEOMEAN = СРГЕОМ +GROWTH = РОСТ +HARMEAN = СРГАРМ +INTERCEPT = ОТРЕЗОК +KURT = ЭКСЦЕСС +LARGE = НАИБОЛЬШИЙ +LINEST = ЛИНЕЙН +LOGEST = ЛГРФПРИБЛ +MAX = МАКС +MAXA = МАКСА +MEDIAN = МЕДИАНА +MIN = МИН +MINA = МИНА +PEARSON = ПИРСОН +PERMUT = ПЕРЕСТ +PROB = ВЕРОЯТНОСТЬ +RSQ = КВПИРСОН +SKEW = СКОС +SLOPE = НАКЛОН +SMALL = НАИМЕНЬШИЙ +STANDARDIZE = НОРМАЛИЗАЦИЯ +STDEVA = СТАНДОТКЛОНА +STDEVPA = СТАНДОТКЛОНПА +STEYX = СТОШYX +TREND = ТЕНДЕНЦИЯ +TRIMMEAN = УРЕЗСРЕДНЕЕ +VARA = ДИСПА +VARPA = ДИСПРА + +## +## Текстовые функции (Text Functions) +## +ASC = ASC +BAHTTEXT = БАТТЕКСТ +CHAR = СИМВОЛ +CLEAN = ПЕЧСИМВ +CODE = КОДСИМВ +DOLLAR = РУБЛЬ +EXACT = СОВПАД +FIND = НАЙТИ +FINDB = НАЙТИБ +FIXED = ФИКСИРОВАННЫЙ +LEFT = ЛЕВСИМВ +LEFTB = ЛЕВБ +LEN = ДЛСТР +LENB = ДЛИНБ +LOWER = СТРОЧН +MID = ПСТР +MIDB = ПСТРБ +PHONETIC = PHONETIC +PROPER = ПРОПНАЧ +REPLACE = ЗАМЕНИТЬ +REPLACEB = ЗАМЕНИТЬБ +REPT = ПОВТОР +RIGHT = ПРАВСИМВ +RIGHTB = ПРАВБ +SEARCH = ПОИСК +SEARCHB = ПОИСКБ +SUBSTITUTE = ПОДСТАВИТЬ +T = Т +TEXT = ТЕКСТ +TRIM = СЖПРОБЕЛЫ +UPPER = ПРОПИСН +VALUE = ЗНАЧЕН + +## +## (Web Functions) +## + +## +## (Compatibility Functions) +## +BETADIST = БЕТАРАСП +BETAINV = БЕТАОБР +BINOMDIST = БИНОМРАСП +CEILING = ОКРВВЕРХ +CHIDIST = ХИ2РАСП +CHIINV = ХИ2ОБР +CHITEST = ХИ2ТЕСТ +CONCATENATE = СЦЕПИТЬ +CONFIDENCE = ДОВЕРИТ +COVAR = КОВАР +CRITBINOM = КРИТБИНОМ +EXPONDIST = ЭКСПРАСП +FDIST = FРАСП +FINV = FРАСПОБР +FLOOR = ОКРВНИЗ +FORECAST = ПРЕДСКАЗ +FTEST = ФТЕСТ +GAMMADIST = ГАММАРАСП +GAMMAINV = ГАММАОБР +HYPGEOMDIST = ГИПЕРГЕОМЕТ +LOGINV = ЛОГНОРМОБР +LOGNORMDIST = ЛОГНОРМРАСП +MODE = МОДА +NEGBINOMDIST = ОТРБИНОМРАСП +NORMDIST = НОРМРАСП +NORMINV = НОРМОБР +NORMSDIST = НОРМСТРАСП +NORMSINV = НОРМСТОБР +PERCENTILE = ПЕРСЕНТИЛЬ +PERCENTRANK = ПРОЦЕНТРАНГ +POISSON = ПУАССОН +QUARTILE = КВАРТИЛЬ +RANK = РАНГ +STDEV = СТАНДОТКЛОН +STDEVP = СТАНДОТКЛОНП +TDIST = СТЬЮДРАСП +TINV = СТЬЮДРАСПОБР +TTEST = ТТЕСТ +VAR = ДИСП +VARP = ДИСПР +WEIBULL = ВЕЙБУЛЛ +ZTEST = ZТЕСТ diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/cs/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/cs/config new file mode 100644 index 0000000..49f40fc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/cs/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Ceština (Czech) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL +DIV0 = #DĚLENÍ_NULOU! +VALUE = #HODNOTA! +REF = #ODKAZ! +NAME = #NÁZEV? +NUM = #ČÍSLO! +NA = #NENÍ_K_DISPOZICI diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/cs/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/cs/functions new file mode 100644 index 0000000..49c4945 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/cs/functions @@ -0,0 +1,520 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Ceština (Czech) +## +############################################################ + + +## +## Funkce pro práci s datovými krychlemi (Cube Functions) +## +CUBEKPIMEMBER = CUBEKPIMEMBER +CUBEMEMBER = CUBEMEMBER +CUBEMEMBERPROPERTY = CUBEMEMBERPROPERTY +CUBERANKEDMEMBER = CUBERANKEDMEMBER +CUBESET = CUBESET +CUBESETCOUNT = CUBESETCOUNT +CUBEVALUE = CUBEVALUE + +## +## Funkce databáze (Database Functions) +## +DAVERAGE = DPRŮMĚR +DCOUNT = DPOČET +DCOUNTA = DPOČET2 +DGET = DZÍSKAT +DMAX = DMAX +DMIN = DMIN +DPRODUCT = DSOUČIN +DSTDEV = DSMODCH.VÝBĚR +DSTDEVP = DSMODCH +DSUM = DSUMA +DVAR = DVAR.VÝBĚR +DVARP = DVAR + +## +## Funkce data a času (Date & Time Functions) +## +DATE = DATUM +DATEVALUE = DATUMHODN +DAY = DEN +DAYS = DAYS +DAYS360 = ROK360 +EDATE = EDATE +EOMONTH = EOMONTH +HOUR = HODINA +ISOWEEKNUM = ISOWEEKNUM +MINUTE = MINUTA +MONTH = MĚSÍC +NETWORKDAYS = NETWORKDAYS +NETWORKDAYS.INTL = NETWORKDAYS.INTL +NOW = NYNÍ +SECOND = SEKUNDA +TIME = ČAS +TIMEVALUE = ČASHODN +TODAY = DNES +WEEKDAY = DENTÝDNE +WEEKNUM = WEEKNUM +WORKDAY = WORKDAY +WORKDAY.INTL = WORKDAY.INTL +YEAR = ROK +YEARFRAC = YEARFRAC + +## +## Inženýrské funkce (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BIN2DEC +BIN2HEX = BIN2HEX +BIN2OCT = BIN2OCT +BITAND = BITAND +BITLSHIFT = BITLSHIFT +BITOR = BITOR +BITRSHIFT = BITRSHIFT +BITXOR = BITXOR +COMPLEX = COMPLEX +CONVERT = CONVERT +DEC2BIN = DEC2BIN +DEC2HEX = DEC2HEX +DEC2OCT = DEC2OCT +DELTA = DELTA +ERF = ERF +ERF.PRECISE = ERF.PRECISE +ERFC = ERFC +ERFC.PRECISE = ERFC.PRECISE +GESTEP = GESTEP +HEX2BIN = HEX2BIN +HEX2DEC = HEX2DEC +HEX2OCT = HEX2OCT +IMABS = IMABS +IMAGINARY = IMAGINARY +IMARGUMENT = IMARGUMENT +IMCONJUGATE = IMCONJUGATE +IMCOS = IMCOS +IMCOSH = IMCOSH +IMCOT = IMCOT +IMCSC = IMCSC +IMCSCH = IMCSCH +IMDIV = IMDIV +IMEXP = IMEXP +IMLN = IMLN +IMLOG10 = IMLOG10 +IMLOG2 = IMLOG2 +IMPOWER = IMPOWER +IMPRODUCT = IMPRODUCT +IMREAL = IMREAL +IMSEC = IMSEC +IMSECH = IMSECH +IMSIN = IMSIN +IMSINH = IMSINH +IMSQRT = IMSQRT +IMSUB = IMSUB +IMSUM = IMSUM +IMTAN = IMTAN +OCT2BIN = OCT2BIN +OCT2DEC = OCT2DEC +OCT2HEX = OCT2HEX + +## +## Finanční funkce (Financial Functions) +## +ACCRINT = ACCRINT +ACCRINTM = ACCRINTM +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = COUPDAYBS +COUPDAYS = COUPDAYS +COUPDAYSNC = COUPDAYSNC +COUPNCD = COUPNCD +COUPNUM = COUPNUM +COUPPCD = COUPPCD +CUMIPMT = CUMIPMT +CUMPRINC = CUMPRINC +DB = ODPIS.ZRYCH +DDB = ODPIS.ZRYCH2 +DISC = DISC +DOLLARDE = DOLLARDE +DOLLARFR = DOLLARFR +DURATION = DURATION +EFFECT = EFFECT +FV = BUDHODNOTA +FVSCHEDULE = FVSCHEDULE +INTRATE = INTRATE +IPMT = PLATBA.ÚROK +IRR = MÍRA.VÝNOSNOSTI +ISPMT = ISPMT +MDURATION = MDURATION +MIRR = MOD.MÍRA.VÝNOSNOSTI +NOMINAL = NOMINAL +NPER = POČET.OBDOBÍ +NPV = ČISTÁ.SOUČHODNOTA +ODDFPRICE = ODDFPRICE +ODDFYIELD = ODDFYIELD +ODDLPRICE = ODDLPRICE +ODDLYIELD = ODDLYIELD +PDURATION = PDURATION +PMT = PLATBA +PPMT = PLATBA.ZÁKLAD +PRICE = PRICE +PRICEDISC = PRICEDISC +PRICEMAT = PRICEMAT +PV = SOUČHODNOTA +RATE = ÚROKOVÁ.MÍRA +RECEIVED = RECEIVED +RRI = RRI +SLN = ODPIS.LIN +SYD = ODPIS.NELIN +TBILLEQ = TBILLEQ +TBILLPRICE = TBILLPRICE +TBILLYIELD = TBILLYIELD +VDB = ODPIS.ZA.INT +XIRR = XIRR +XNPV = XNPV +YIELD = YIELD +YIELDDISC = YIELDDISC +YIELDMAT = YIELDMAT + +## +## Informační funkce (Information Functions) +## +CELL = POLÍČKO +ERROR.TYPE = CHYBA.TYP +INFO = O.PROSTŘEDÍ +ISBLANK = JE.PRÁZDNÉ +ISERR = JE.CHYBA +ISERROR = JE.CHYBHODN +ISEVEN = ISEVEN +ISFORMULA = ISFORMULA +ISLOGICAL = JE.LOGHODN +ISNA = JE.NEDEF +ISNONTEXT = JE.NETEXT +ISNUMBER = JE.ČISLO +ISODD = ISODD +ISREF = JE.ODKAZ +ISTEXT = JE.TEXT +N = N +NA = NEDEF +SHEET = SHEET +SHEETS = SHEETS +TYPE = TYP + +## +## Logické funkce (Logical Functions) +## +AND = A +FALSE = NEPRAVDA +IF = KDYŽ +IFERROR = IFERROR +IFNA = IFNA +IFS = IFS +NOT = NE +OR = NEBO +SWITCH = SWITCH +TRUE = PRAVDA +XOR = XOR + +## +## Vyhledávací funkce a funkce pro odkazy (Lookup & Reference Functions) +## +ADDRESS = ODKAZ +AREAS = POČET.BLOKŮ +CHOOSE = ZVOLIT +COLUMN = SLOUPEC +COLUMNS = SLOUPCE +FORMULATEXT = FORMULATEXT +GETPIVOTDATA = ZÍSKATKONTDATA +HLOOKUP = VVYHLEDAT +HYPERLINK = HYPERTEXTOVÝ.ODKAZ +INDEX = INDEX +INDIRECT = NEPŘÍMÝ.ODKAZ +LOOKUP = VYHLEDAT +MATCH = POZVYHLEDAT +OFFSET = POSUN +ROW = ŘÁDEK +ROWS = ŘÁDKY +RTD = RTD +TRANSPOSE = TRANSPOZICE +VLOOKUP = SVYHLEDAT + +## +## Matematické a trigonometrické funkce (Math & Trig Functions) +## +ABS = ABS +ACOS = ARCCOS +ACOSH = ARCCOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = AGGREGATE +ARABIC = ARABIC +ASIN = ARCSIN +ASINH = ARCSINH +ATAN = ARCTG +ATAN2 = ARCTG2 +ATANH = ARCTGH +BASE = BASE +CEILING.MATH = CEILING.MATH +COMBIN = KOMBINACE +COMBINA = COMBINA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DECIMAL +DEGREES = DEGREES +EVEN = ZAOKROUHLIT.NA.SUDÉ +EXP = EXP +FACT = FAKTORIÁL +FACTDOUBLE = FACTDOUBLE +FLOOR.MATH = FLOOR.MATH +GCD = GCD +INT = CELÁ.ČÁST +LCM = LCM +LN = LN +LOG = LOGZ +LOG10 = LOG +MDETERM = DETERMINANT +MINVERSE = INVERZE +MMULT = SOUČIN.MATIC +MOD = MOD +MROUND = MROUND +MULTINOMIAL = MULTINOMIAL +MUNIT = MUNIT +ODD = ZAOKROUHLIT.NA.LICHÉ +PI = PI +POWER = POWER +PRODUCT = SOUČIN +QUOTIENT = QUOTIENT +RADIANS = RADIANS +RAND = NÁHČÍSLO +RANDBETWEEN = RANDBETWEEN +ROMAN = ROMAN +ROUND = ZAOKROUHLIT +ROUNDDOWN = ROUNDDOWN +ROUNDUP = ROUNDUP +SEC = SEC +SECH = SECH +SERIESSUM = SERIESSUM +SIGN = SIGN +SIN = SIN +SINH = SINH +SQRT = ODMOCNINA +SQRTPI = SQRTPI +SUBTOTAL = SUBTOTAL +SUM = SUMA +SUMIF = SUMIF +SUMIFS = SUMIFS +SUMPRODUCT = SOUČIN.SKALÁRNÍ +SUMSQ = SUMA.ČTVERCŮ +SUMX2MY2 = SUMX2MY2 +SUMX2PY2 = SUMX2PY2 +SUMXMY2 = SUMXMY2 +TAN = TG +TANH = TGH +TRUNC = USEKNOUT + +## +## Statistické funkce (Statistical Functions) +## +AVEDEV = PRŮMODCHYLKA +AVERAGE = PRŮMĚR +AVERAGEA = AVERAGEA +AVERAGEIF = AVERAGEIF +AVERAGEIFS = AVERAGEIFS +BETA.DIST = BETA.DIST +BETA.INV = BETA.INV +BINOM.DIST = BINOM.DIST +BINOM.DIST.RANGE = BINOM.DIST.RANGE +BINOM.INV = BINOM.INV +CHISQ.DIST = CHISQ.DIST +CHISQ.DIST.RT = CHISQ.DIST.RT +CHISQ.INV = CHISQ.INV +CHISQ.INV.RT = CHISQ.INV.RT +CHISQ.TEST = CHISQ.TEST +CONFIDENCE.NORM = CONFIDENCE.NORM +CONFIDENCE.T = CONFIDENCE.T +CORREL = CORREL +COUNT = POČET +COUNTA = POČET2 +COUNTBLANK = COUNTBLANK +COUNTIF = COUNTIF +COUNTIFS = COUNTIFS +COVARIANCE.P = COVARIANCE.P +COVARIANCE.S = COVARIANCE.S +DEVSQ = DEVSQ +EXPON.DIST = EXPON.DIST +F.DIST = F.DIST +F.DIST.RT = F.DIST.RT +F.INV = F.INV +F.INV.RT = F.INV.RT +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = FORECAST.ETS +FORECAST.ETS.CONFINT = FORECAST.ETS.CONFINT +FORECAST.ETS.SEASONALITY = FORECAST.ETS.SEASONALITY +FORECAST.ETS.STAT = FORECAST.ETS.STAT +FORECAST.LINEAR = FORECAST.LINEAR +FREQUENCY = ČETNOSTI +GAMMA = GAMMA +GAMMA.DIST = GAMMA.DIST +GAMMA.INV = GAMMA.INV +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.PRECISE +GAUSS = GAUSS +GEOMEAN = GEOMEAN +GROWTH = LOGLINTREND +HARMEAN = HARMEAN +HYPGEOM.DIST = HYPGEOM.DIST +INTERCEPT = INTERCEPT +KURT = KURT +LARGE = LARGE +LINEST = LINREGRESE +LOGEST = LOGLINREGRESE +LOGNORM.DIST = LOGNORM.DIST +LOGNORM.INV = LOGNORM.INV +MAX = MAX +MAXA = MAXA +MAXIFS = MAXIFS +MEDIAN = MEDIAN +MIN = MIN +MINA = MINA +MINIFS = MINIFS +MODE.MULT = MODE.MULT +MODE.SNGL = MODE.SNGL +NEGBINOM.DIST = NEGBINOM.DIST +NORM.DIST = NORM.DIST +NORM.INV = NORM.INV +NORM.S.DIST = NORM.S.DIST +NORM.S.INV = NORM.S.INV +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTIL.EXC +PERCENTILE.INC = PERCENTIL.INC +PERCENTRANK.EXC = PERCENTRANK.EXC +PERCENTRANK.INC = PERCENTRANK.INC +PERMUT = PERMUTACE +PERMUTATIONA = PERMUTATIONA +PHI = PHI +POISSON.DIST = POISSON.DIST +PROB = PROB +QUARTILE.EXC = QUARTIL.EXC +QUARTILE.INC = QUARTIL.INC +RANK.AVG = RANK.AVG +RANK.EQ = RANK.EQ +RSQ = RKQ +SKEW = SKEW +SKEW.P = SKEW.P +SLOPE = SLOPE +SMALL = SMALL +STANDARDIZE = STANDARDIZE +STDEV.P = SMODCH.P +STDEV.S = SMODCH.VÝBĚR.S +STDEVA = STDEVA +STDEVPA = STDEVPA +STEYX = STEYX +T.DIST = T.DIST +T.DIST.2T = T.DIST.2T +T.DIST.RT = T.DIST.RT +T.INV = T.INV +T.INV.2T = T.INV.2T +T.TEST = T.TEST +TREND = LINTREND +TRIMMEAN = TRIMMEAN +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = WEIBULL.DIST +Z.TEST = Z.TEST + +## +## Textové funkce (Text Functions) +## +BAHTTEXT = BAHTTEXT +CHAR = ZNAK +CLEAN = VYČISTIT +CODE = KÓD +CONCAT = CONCAT +DOLLAR = KČ +EXACT = STEJNÉ +FIND = NAJÍT +FIXED = ZAOKROUHLIT.NA.TEXT +LEFT = ZLEVA +LEN = DÉLKA +LOWER = MALÁ +MID = ČÁST +NUMBERVALUE = NUMBERVALUE +PHONETIC = ZVUKOVÉ +PROPER = VELKÁ2 +REPLACE = NAHRADIT +REPT = OPAKOVAT +RIGHT = ZPRAVA +SEARCH = HLEDAT +SUBSTITUTE = DOSADIT +T = T +TEXT = HODNOTA.NA.TEXT +TEXTJOIN = TEXTJOIN +TRIM = PROČISTIT +UNICHAR = UNICHAR +UNICODE = UNICODE +UPPER = VELKÁ +VALUE = HODNOTA + +## +## Webové funkce (Web Functions) +## +ENCODEURL = ENCODEURL +FILTERXML = FILTERXML +WEBSERVICE = WEBSERVICE + +## +## Funkce pro kompatibilitu (Compatibility Functions) +## +BETADIST = BETADIST +BETAINV = BETAINV +BINOMDIST = BINOMDIST +CEILING = ZAOKR.NAHORU +CHIDIST = CHIDIST +CHIINV = CHIINV +CHITEST = CHITEST +CONCATENATE = CONCATENATE +CONFIDENCE = CONFIDENCE +COVAR = COVAR +CRITBINOM = CRITBINOM +EXPONDIST = EXPONDIST +FDIST = FDIST +FINV = FINV +FLOOR = ZAOKR.DOLŮ +FORECAST = FORECAST +FTEST = FTEST +GAMMADIST = GAMMADIST +GAMMAINV = GAMMAINV +HYPGEOMDIST = HYPGEOMDIST +LOGINV = LOGINV +LOGNORMDIST = LOGNORMDIST +MODE = MODE +NEGBINOMDIST = NEGBINOMDIST +NORMDIST = NORMDIST +NORMINV = NORMINV +NORMSDIST = NORMSDIST +NORMSINV = NORMSINV +PERCENTILE = PERCENTIL +PERCENTRANK = PERCENTRANK +POISSON = POISSON +QUARTILE = QUARTIL +RANK = RANK +STDEV = SMODCH.VÝBĚR +STDEVP = SMODCH +TDIST = TDIST +TINV = TINV +TTEST = TTEST +VAR = VAR.VÝBĚR +VARP = VAR +WEIBULL = WEIBULL +ZTEST = ZTEST diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/da/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/da/config new file mode 100644 index 0000000..284b249 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/da/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Dansk (Danish) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #NUL! +DIV0 +VALUE = #VÆRDI! +REF = #REFERENCE! +NAME = #NAVN? +NUM +NA = #I/T diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/da/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/da/functions new file mode 100644 index 0000000..03b68c9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/da/functions @@ -0,0 +1,538 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Dansk (Danish) +## +############################################################ + + +## +## Kubefunktioner (Cube Functions) +## +CUBEKPIMEMBER = KUBE.KPI.MEDLEM +CUBEMEMBER = KUBEMEDLEM +CUBEMEMBERPROPERTY = KUBEMEDLEM.EGENSKAB +CUBERANKEDMEMBER = KUBERANGERET.MEDLEM +CUBESET = KUBESÆT +CUBESETCOUNT = KUBESÆT.ANTAL +CUBEVALUE = KUBEVÆRDI + +## +## Databasefunktioner (Database Functions) +## +DAVERAGE = DMIDDEL +DCOUNT = DTÆL +DCOUNTA = DTÆLV +DGET = DHENT +DMAX = DMAKS +DMIN = DMIN +DPRODUCT = DPRODUKT +DSTDEV = DSTDAFV +DSTDEVP = DSTDAFVP +DSUM = DSUM +DVAR = DVARIANS +DVARP = DVARIANSP + +## +## Dato- og klokkeslætfunktioner (Date & Time Functions) +## +DATE = DATO +DATEDIF = DATO.FORSKEL +DATESTRING = DATOSTRENG +DATEVALUE = DATOVÆRDI +DAY = DAG +DAYS = DAGE +DAYS360 = DAGE360 +EDATE = EDATO +EOMONTH = SLUT.PÅ.MÅNED +HOUR = TIME +ISOWEEKNUM = ISOUGE.NR +MINUTE = MINUT +MONTH = MÅNED +NETWORKDAYS = ANTAL.ARBEJDSDAGE +NETWORKDAYS.INTL = ANTAL.ARBEJDSDAGE.INTL +NOW = NU +SECOND = SEKUND +THAIDAYOFWEEK = THAILANDSKUGEDAG +THAIMONTHOFYEAR = THAILANDSKMÅNED +THAIYEAR = THAILANDSKÅR +TIME = TID +TIMEVALUE = TIDSVÆRDI +TODAY = IDAG +WEEKDAY = UGEDAG +WEEKNUM = UGE.NR +WORKDAY = ARBEJDSDAG +WORKDAY.INTL = ARBEJDSDAG.INTL +YEAR = ÅR +YEARFRAC = ÅR.BRØK + +## +## Tekniske funktioner (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BIN.TIL.DEC +BIN2HEX = BIN.TIL.HEX +BIN2OCT = BIN.TIL.OKT +BITAND = BITOG +BITLSHIFT = BITLSKIFT +BITOR = BITELLER +BITRSHIFT = BITRSKIFT +BITXOR = BITXELLER +COMPLEX = KOMPLEKS +CONVERT = KONVERTER +DEC2BIN = DEC.TIL.BIN +DEC2HEX = DEC.TIL.HEX +DEC2OCT = DEC.TIL.OKT +DELTA = DELTA +ERF = FEJLFUNK +ERF.PRECISE = ERF.PRECISE +ERFC = FEJLFUNK.KOMP +ERFC.PRECISE = ERFC.PRECISE +GESTEP = GETRIN +HEX2BIN = HEX.TIL.BIN +HEX2DEC = HEX.TIL.DEC +HEX2OCT = HEX.TIL.OKT +IMABS = IMAGABS +IMAGINARY = IMAGINÆR +IMARGUMENT = IMAGARGUMENT +IMCONJUGATE = IMAGKONJUGERE +IMCOS = IMAGCOS +IMCOSH = IMAGCOSH +IMCOT = IMAGCOT +IMCSC = IMAGCSC +IMCSCH = IMAGCSCH +IMDIV = IMAGDIV +IMEXP = IMAGEKSP +IMLN = IMAGLN +IMLOG10 = IMAGLOG10 +IMLOG2 = IMAGLOG2 +IMPOWER = IMAGPOTENS +IMPRODUCT = IMAGPRODUKT +IMREAL = IMAGREELT +IMSEC = IMAGSEC +IMSECH = IMAGSECH +IMSIN = IMAGSIN +IMSINH = IMAGSINH +IMSQRT = IMAGKVROD +IMSUB = IMAGSUB +IMSUM = IMAGSUM +IMTAN = IMAGTAN +OCT2BIN = OKT.TIL.BIN +OCT2DEC = OKT.TIL.DEC +OCT2HEX = OKT.TIL.HEX + +## +## Finansielle funktioner (Financial Functions) +## +ACCRINT = PÅLØBRENTE +ACCRINTM = PÅLØBRENTE.UDLØB +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = KUPONDAGE.SA +COUPDAYS = KUPONDAGE.A +COUPDAYSNC = KUPONDAGE.ANK +COUPNCD = KUPONDAG.NÆSTE +COUPNUM = KUPONBETALINGER +COUPPCD = KUPONDAG.FORRIGE +CUMIPMT = AKKUM.RENTE +CUMPRINC = AKKUM.HOVEDSTOL +DB = DB +DDB = DSA +DISC = DISKONTO +DOLLARDE = KR.DECIMAL +DOLLARFR = KR.BRØK +DURATION = VARIGHED +EFFECT = EFFEKTIV.RENTE +FV = FV +FVSCHEDULE = FVTABEL +INTRATE = RENTEFOD +IPMT = R.YDELSE +IRR = IA +ISPMT = ISPMT +MDURATION = MVARIGHED +MIRR = MIA +NOMINAL = NOMINEL +NPER = NPER +NPV = NUTIDSVÆRDI +ODDFPRICE = ULIGE.KURS.PÅLYDENDE +ODDFYIELD = ULIGE.FØRSTE.AFKAST +ODDLPRICE = ULIGE.SIDSTE.KURS +ODDLYIELD = ULIGE.SIDSTE.AFKAST +PDURATION = PVARIGHED +PMT = YDELSE +PPMT = H.YDELSE +PRICE = KURS +PRICEDISC = KURS.DISKONTO +PRICEMAT = KURS.UDLØB +PV = NV +RATE = RENTE +RECEIVED = MODTAGET.VED.UDLØB +RRI = RRI +SLN = LA +SYD = ÅRSAFSKRIVNING +TBILLEQ = STATSOBLIGATION +TBILLPRICE = STATSOBLIGATION.KURS +TBILLYIELD = STATSOBLIGATION.AFKAST +VDB = VSA +XIRR = INTERN.RENTE +XNPV = NETTO.NUTIDSVÆRDI +YIELD = AFKAST +YIELDDISC = AFKAST.DISKONTO +YIELDMAT = AFKAST.UDLØBSDATO + +## +## Informationsfunktioner (Information Functions) +## +CELL = CELLE +ERROR.TYPE = FEJLTYPE +INFO = INFO +ISBLANK = ER.TOM +ISERR = ER.FJL +ISERROR = ER.FEJL +ISEVEN = ER.LIGE +ISFORMULA = ER.FORMEL +ISLOGICAL = ER.LOGISK +ISNA = ER.IKKE.TILGÆNGELIG +ISNONTEXT = ER.IKKE.TEKST +ISNUMBER = ER.TAL +ISODD = ER.ULIGE +ISREF = ER.REFERENCE +ISTEXT = ER.TEKST +N = TAL +NA = IKKE.TILGÆNGELIG +SHEET = ARK +SHEETS = ARK.FLERE +TYPE = VÆRDITYPE + +## +## Logiske funktioner (Logical Functions) +## +AND = OG +FALSE = FALSK +IF = HVIS +IFERROR = HVIS.FEJL +IFNA = HVISIT +IFS = HVISER +NOT = IKKE +OR = ELLER +SWITCH = SKIFT +TRUE = SAND +XOR = XELLER + +## +## Opslags- og referencefunktioner (Lookup & Reference Functions) +## +ADDRESS = ADRESSE +AREAS = OMRÅDER +CHOOSE = VÆLG +COLUMN = KOLONNE +COLUMNS = KOLONNER +FORMULATEXT = FORMELTEKST +GETPIVOTDATA = GETPIVOTDATA +HLOOKUP = VOPSLAG +HYPERLINK = HYPERLINK +INDEX = INDEKS +INDIRECT = INDIREKTE +LOOKUP = SLÅ.OP +MATCH = SAMMENLIGN +OFFSET = FORSKYDNING +ROW = RÆKKE +ROWS = RÆKKER +RTD = RTD +TRANSPOSE = TRANSPONER +VLOOKUP = LOPSLAG +*RC = RK + +## +## Matematiske og trigonometriske funktioner (Math & Trig Functions) +## +ABS = ABS +ACOS = ARCCOS +ACOSH = ARCCOSH +ACOT = ARCCOT +ACOTH = ARCCOTH +AGGREGATE = SAMLING +ARABIC = ARABISK +ASIN = ARCSIN +ASINH = ARCSINH +ATAN = ARCTAN +ATAN2 = ARCTAN2 +ATANH = ARCTANH +BASE = BASIS +CEILING.MATH = LOFT.MAT +CEILING.PRECISE = LOFT.PRECISE +COMBIN = KOMBIN +COMBINA = KOMBINA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DECIMAL +DEGREES = GRADER +ECMA.CEILING = ECMA.LOFT +EVEN = LIGE +EXP = EKSP +FACT = FAKULTET +FACTDOUBLE = DOBBELT.FAKULTET +FLOOR.MATH = AFRUND.BUND.MAT +FLOOR.PRECISE = AFRUND.GULV.PRECISE +GCD = STØRSTE.FÆLLES.DIVISOR +INT = HELTAL +ISO.CEILING = ISO.LOFT +LCM = MINDSTE.FÆLLES.MULTIPLUM +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MDETERM +MINVERSE = MINVERT +MMULT = MPRODUKT +MOD = REST +MROUND = MAFRUND +MULTINOMIAL = MULTINOMIAL +MUNIT = MENHED +ODD = ULIGE +PI = PI +POWER = POTENS +PRODUCT = PRODUKT +QUOTIENT = KVOTIENT +RADIANS = RADIANER +RAND = SLUMP +RANDBETWEEN = SLUMPMELLEM +ROMAN = ROMERTAL +ROUND = AFRUND +ROUNDBAHTDOWN = RUNDBAHTNED +ROUNDBAHTUP = RUNDBAHTOP +ROUNDDOWN = RUND.NED +ROUNDUP = RUND.OP +SEC = SEC +SECH = SECH +SERIESSUM = SERIESUM +SIGN = FORTEGN +SIN = SIN +SINH = SINH +SQRT = KVROD +SQRTPI = KVRODPI +SUBTOTAL = SUBTOTAL +SUM = SUM +SUMIF = SUM.HVIS +SUMIFS = SUM.HVISER +SUMPRODUCT = SUMPRODUKT +SUMSQ = SUMKV +SUMX2MY2 = SUMX2MY2 +SUMX2PY2 = SUMX2PY2 +SUMXMY2 = SUMXMY2 +TAN = TAN +TANH = TANH +TRUNC = AFKORT + +## +## Statistiske funktioner (Statistical Functions) +## +AVEDEV = MAD +AVERAGE = MIDDEL +AVERAGEA = MIDDELV +AVERAGEIF = MIDDEL.HVIS +AVERAGEIFS = MIDDEL.HVISER +BETA.DIST = BETA.FORDELING +BETA.INV = BETA.INV +BINOM.DIST = BINOMIAL.FORDELING +BINOM.DIST.RANGE = BINOMIAL.DIST.INTERVAL +BINOM.INV = BINOMIAL.INV +CHISQ.DIST = CHI2.FORDELING +CHISQ.DIST.RT = CHI2.FORD.RT +CHISQ.INV = CHI2.INV +CHISQ.INV.RT = CHI2.INV.RT +CHISQ.TEST = CHI2.TEST +CONFIDENCE.NORM = KONFIDENS.NORM +CONFIDENCE.T = KONFIDENST +CORREL = KORRELATION +COUNT = TÆL +COUNTA = TÆLV +COUNTBLANK = ANTAL.BLANKE +COUNTIF = TÆL.HVIS +COUNTIFS = TÆL.HVISER +COVARIANCE.P = KOVARIANS.P +COVARIANCE.S = KOVARIANS.S +DEVSQ = SAK +EXPON.DIST = EKSP.FORDELING +F.DIST = F.FORDELING +F.DIST.RT = F.FORDELING.RT +F.INV = F.INV +F.INV.RT = F.INV.RT +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = PROGNOSE.ETS +FORECAST.ETS.CONFINT = PROGNOSE.ETS.CONFINT +FORECAST.ETS.SEASONALITY = PROGNOSE.ETS.SÆSONUDSVING +FORECAST.ETS.STAT = PROGNOSE.ETS.STAT +FORECAST.LINEAR = PROGNOSE.LINEÆR +FREQUENCY = FREKVENS +GAMMA = GAMMA +GAMMA.DIST = GAMMA.FORDELING +GAMMA.INV = GAMMA.INV +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.PRECISE +GAUSS = GAUSS +GEOMEAN = GEOMIDDELVÆRDI +GROWTH = FORØGELSE +HARMEAN = HARMIDDELVÆRDI +HYPGEOM.DIST = HYPGEO.FORDELING +INTERCEPT = SKÆRING +KURT = TOPSTEJL +LARGE = STØRSTE +LINEST = LINREGR +LOGEST = LOGREGR +LOGNORM.DIST = LOGNORM.FORDELING +LOGNORM.INV = LOGNORM.INV +MAX = MAKS +MAXA = MAKSV +MAXIFS = MAKSHVISER +MEDIAN = MEDIAN +MIN = MIN +MINA = MINV +MINIFS = MINHVISER +MODE.MULT = HYPPIGST.FLERE +MODE.SNGL = HYPPIGST.ENKELT +NEGBINOM.DIST = NEGBINOM.FORDELING +NORM.DIST = NORMAL.FORDELING +NORM.INV = NORM.INV +NORM.S.DIST = STANDARD.NORM.FORDELING +NORM.S.INV = STANDARD.NORM.INV +PEARSON = PEARSON +PERCENTILE.EXC = FRAKTIL.UDELAD +PERCENTILE.INC = FRAKTIL.MEDTAG +PERCENTRANK.EXC = PROCENTPLADS.UDELAD +PERCENTRANK.INC = PROCENTPLADS.MEDTAG +PERMUT = PERMUT +PERMUTATIONA = PERMUTATIONA +PHI = PHI +POISSON.DIST = POISSON.FORDELING +PROB = SANDSYNLIGHED +QUARTILE.EXC = KVARTIL.UDELAD +QUARTILE.INC = KVARTIL.MEDTAG +RANK.AVG = PLADS.GNSN +RANK.EQ = PLADS.LIGE +RSQ = FORKLARINGSGRAD +SKEW = SKÆVHED +SKEW.P = SKÆVHED.P +SLOPE = STIGNING +SMALL = MINDSTE +STANDARDIZE = STANDARDISER +STDEV.P = STDAFV.P +STDEV.S = STDAFV.S +STDEVA = STDAFVV +STDEVPA = STDAFVPV +STEYX = STFYX +T.DIST = T.FORDELING +T.DIST.2T = T.FORDELING.2T +T.DIST.RT = T.FORDELING.RT +T.INV = T.INV +T.INV.2T = T.INV.2T +T.TEST = T.TEST +TREND = TENDENS +TRIMMEAN = TRIMMIDDELVÆRDI +VAR.P = VARIANS.P +VAR.S = VARIANS.S +VARA = VARIANSV +VARPA = VARIANSPV +WEIBULL.DIST = WEIBULL.FORDELING +Z.TEST = Z.TEST + +## +## Tekstfunktioner (Text Functions) +## +BAHTTEXT = BAHTTEKST +CHAR = TEGN +CLEAN = RENS +CODE = KODE +CONCAT = CONCAT +DOLLAR = KR +EXACT = EKSAKT +FIND = FIND +FIXED = FAST +ISTHAIDIGIT = ERTHAILANDSKCIFFER +LEFT = VENSTRE +LEN = LÆNGDE +LOWER = SMÅ.BOGSTAVER +MID = MIDT +NUMBERSTRING = TALSTRENG +NUMBERVALUE = TALVÆRDI +PHONETIC = FONETISK +PROPER = STORT.FORBOGSTAV +REPLACE = ERSTAT +REPT = GENTAG +RIGHT = HØJRE +SEARCH = SØG +SUBSTITUTE = UDSKIFT +T = T +TEXT = TEKST +TEXTJOIN = TEKST.KOMBINER +THAIDIGIT = THAILANDSKCIFFER +THAINUMSOUND = THAILANDSKNUMLYD +THAINUMSTRING = THAILANDSKNUMSTRENG +THAISTRINGLENGTH = THAILANDSKSTRENGLÆNGDE +TRIM = FJERN.OVERFLØDIGE.BLANKE +UNICHAR = UNICHAR +UNICODE = UNICODE +UPPER = STORE.BOGSTAVER +VALUE = VÆRDI + +## +## Webfunktioner (Web Functions) +## +ENCODEURL = KODNINGSURL +FILTERXML = FILTRERXML +WEBSERVICE = WEBTJENESTE + +## +## Kompatibilitetsfunktioner (Compatibility Functions) +## +BETADIST = BETAFORDELING +BETAINV = BETAINV +BINOMDIST = BINOMIALFORDELING +CEILING = AFRUND.LOFT +CHIDIST = CHIFORDELING +CHIINV = CHIINV +CHITEST = CHITEST +CONCATENATE = SAMMENKÆDNING +CONFIDENCE = KONFIDENSINTERVAL +COVAR = KOVARIANS +CRITBINOM = KRITBINOM +EXPONDIST = EKSPFORDELING +FDIST = FFORDELING +FINV = FINV +FLOOR = AFRUND.GULV +FORECAST = PROGNOSE +FTEST = FTEST +GAMMADIST = GAMMAFORDELING +GAMMAINV = GAMMAINV +HYPGEOMDIST = HYPGEOFORDELING +LOGINV = LOGINV +LOGNORMDIST = LOGNORMFORDELING +MODE = HYPPIGST +NEGBINOMDIST = NEGBINOMFORDELING +NORMDIST = NORMFORDELING +NORMINV = NORMINV +NORMSDIST = STANDARDNORMFORDELING +NORMSINV = STANDARDNORMINV +PERCENTILE = FRAKTIL +PERCENTRANK = PROCENTPLADS +POISSON = POISSON +QUARTILE = KVARTIL +RANK = PLADS +STDEV = STDAFV +STDEVP = STDAFVP +TDIST = TFORDELING +TINV = TINV +TTEST = TTEST +VAR = VARIANS +VARP = VARIANSP +WEIBULL = WEIBULL +ZTEST = ZTEST diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/de/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/de/config new file mode 100644 index 0000000..4ca2b82 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/de/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Deutsch (German) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL +DIV0 +VALUE = #WERT! +REF = #BEZUG! +NAME +NUM = #ZAHL! +NA = #NV diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/de/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/de/functions new file mode 100644 index 0000000..d49fc5f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/de/functions @@ -0,0 +1,534 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Deutsch (German) +## +############################################################ + + +## +## Cubefunktionen (Cube Functions) +## +CUBEKPIMEMBER = CUBEKPIELEMENT +CUBEMEMBER = CUBEELEMENT +CUBEMEMBERPROPERTY = CUBEELEMENTEIGENSCHAFT +CUBERANKEDMEMBER = CUBERANGELEMENT +CUBESET = CUBEMENGE +CUBESETCOUNT = CUBEMENGENANZAHL +CUBEVALUE = CUBEWERT + +## +## Datenbankfunktionen (Database Functions) +## +DAVERAGE = DBMITTELWERT +DCOUNT = DBANZAHL +DCOUNTA = DBANZAHL2 +DGET = DBAUSZUG +DMAX = DBMAX +DMIN = DBMIN +DPRODUCT = DBPRODUKT +DSTDEV = DBSTDABW +DSTDEVP = DBSTDABWN +DSUM = DBSUMME +DVAR = DBVARIANZ +DVARP = DBVARIANZEN + +## +## Datums- und Uhrzeitfunktionen (Date & Time Functions) +## +DATE = DATUM +DATEVALUE = DATWERT +DAY = TAG +DAYS = TAGE +DAYS360 = TAGE360 +EDATE = EDATUM +EOMONTH = MONATSENDE +HOUR = STUNDE +ISOWEEKNUM = ISOKALENDERWOCHE +MINUTE = MINUTE +MONTH = MONAT +NETWORKDAYS = NETTOARBEITSTAGE +NETWORKDAYS.INTL = NETTOARBEITSTAGE.INTL +NOW = JETZT +SECOND = SEKUNDE +THAIDAYOFWEEK = THAIWOCHENTAG +THAIMONTHOFYEAR = THAIMONATDESJAHRES +THAIYEAR = THAIJAHR +TIME = ZEIT +TIMEVALUE = ZEITWERT +TODAY = HEUTE +WEEKDAY = WOCHENTAG +WEEKNUM = KALENDERWOCHE +WORKDAY = ARBEITSTAG +WORKDAY.INTL = ARBEITSTAG.INTL +YEAR = JAHR +YEARFRAC = BRTEILJAHRE + +## +## Technische Funktionen (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BININDEZ +BIN2HEX = BININHEX +BIN2OCT = BININOKT +BITAND = BITUND +BITLSHIFT = BITLVERSCHIEB +BITOR = BITODER +BITRSHIFT = BITRVERSCHIEB +BITXOR = BITXODER +COMPLEX = KOMPLEXE +CONVERT = UMWANDELN +DEC2BIN = DEZINBIN +DEC2HEX = DEZINHEX +DEC2OCT = DEZINOKT +DELTA = DELTA +ERF = GAUSSFEHLER +ERF.PRECISE = GAUSSF.GENAU +ERFC = GAUSSFKOMPL +ERFC.PRECISE = GAUSSFKOMPL.GENAU +GESTEP = GGANZZAHL +HEX2BIN = HEXINBIN +HEX2DEC = HEXINDEZ +HEX2OCT = HEXINOKT +IMABS = IMABS +IMAGINARY = IMAGINÄRTEIL +IMARGUMENT = IMARGUMENT +IMCONJUGATE = IMKONJUGIERTE +IMCOS = IMCOS +IMCOSH = IMCOSHYP +IMCOT = IMCOT +IMCSC = IMCOSEC +IMCSCH = IMCOSECHYP +IMDIV = IMDIV +IMEXP = IMEXP +IMLN = IMLN +IMLOG10 = IMLOG10 +IMLOG2 = IMLOG2 +IMPOWER = IMAPOTENZ +IMPRODUCT = IMPRODUKT +IMREAL = IMREALTEIL +IMSEC = IMSEC +IMSECH = IMSECHYP +IMSIN = IMSIN +IMSINH = IMSINHYP +IMSQRT = IMWURZEL +IMSUB = IMSUB +IMSUM = IMSUMME +IMTAN = IMTAN +OCT2BIN = OKTINBIN +OCT2DEC = OKTINDEZ +OCT2HEX = OKTINHEX + +## +## Finanzmathematische Funktionen (Financial Functions) +## +ACCRINT = AUFGELZINS +ACCRINTM = AUFGELZINSF +AMORDEGRC = AMORDEGRK +AMORLINC = AMORLINEARK +COUPDAYBS = ZINSTERMTAGVA +COUPDAYS = ZINSTERMTAGE +COUPDAYSNC = ZINSTERMTAGNZ +COUPNCD = ZINSTERMNZ +COUPNUM = ZINSTERMZAHL +COUPPCD = ZINSTERMVZ +CUMIPMT = KUMZINSZ +CUMPRINC = KUMKAPITAL +DB = GDA2 +DDB = GDA +DISC = DISAGIO +DOLLARDE = NOTIERUNGDEZ +DOLLARFR = NOTIERUNGBRU +DURATION = DURATION +EFFECT = EFFEKTIV +FV = ZW +FVSCHEDULE = ZW2 +INTRATE = ZINSSATZ +IPMT = ZINSZ +IRR = IKV +ISPMT = ISPMT +MDURATION = MDURATION +MIRR = QIKV +NOMINAL = NOMINAL +NPER = ZZR +NPV = NBW +ODDFPRICE = UNREGER.KURS +ODDFYIELD = UNREGER.REND +ODDLPRICE = UNREGLE.KURS +ODDLYIELD = UNREGLE.REND +PDURATION = PDURATION +PMT = RMZ +PPMT = KAPZ +PRICE = KURS +PRICEDISC = KURSDISAGIO +PRICEMAT = KURSFÄLLIG +PV = BW +RATE = ZINS +RECEIVED = AUSZAHLUNG +RRI = ZSATZINVEST +SLN = LIA +SYD = DIA +TBILLEQ = TBILLÄQUIV +TBILLPRICE = TBILLKURS +TBILLYIELD = TBILLRENDITE +VDB = VDB +XIRR = XINTZINSFUSS +XNPV = XKAPITALWERT +YIELD = RENDITE +YIELDDISC = RENDITEDIS +YIELDMAT = RENDITEFÄLL + +## +## Informationsfunktionen (Information Functions) +## +CELL = ZELLE +ERROR.TYPE = FEHLER.TYP +INFO = INFO +ISBLANK = ISTLEER +ISERR = ISTFEHL +ISERROR = ISTFEHLER +ISEVEN = ISTGERADE +ISFORMULA = ISTFORMEL +ISLOGICAL = ISTLOG +ISNA = ISTNV +ISNONTEXT = ISTKTEXT +ISNUMBER = ISTZAHL +ISODD = ISTUNGERADE +ISREF = ISTBEZUG +ISTEXT = ISTTEXT +N = N +NA = NV +SHEET = BLATT +SHEETS = BLÄTTER +TYPE = TYP + +## +## Logische Funktionen (Logical Functions) +## +AND = UND +FALSE = FALSCH +IF = WENN +IFERROR = WENNFEHLER +IFNA = WENNNV +IFS = WENNS +NOT = NICHT +OR = ODER +SWITCH = ERSTERWERT +TRUE = WAHR +XOR = XODER + +## +## Nachschlage- und Verweisfunktionen (Lookup & Reference Functions) +## +ADDRESS = ADRESSE +AREAS = BEREICHE +CHOOSE = WAHL +COLUMN = SPALTE +COLUMNS = SPALTEN +FORMULATEXT = FORMELTEXT +GETPIVOTDATA = PIVOTDATENZUORDNEN +HLOOKUP = WVERWEIS +HYPERLINK = HYPERLINK +INDEX = INDEX +INDIRECT = INDIREKT +LOOKUP = VERWEIS +MATCH = VERGLEICH +OFFSET = BEREICH.VERSCHIEBEN +ROW = ZEILE +ROWS = ZEILEN +RTD = RTD +TRANSPOSE = MTRANS +VLOOKUP = SVERWEIS +*RC = ZS + +## +## Mathematische und trigonometrische Funktionen (Math & Trig Functions) +## +ABS = ABS +ACOS = ARCCOS +ACOSH = ARCCOSHYP +ACOT = ARCCOT +ACOTH = ARCCOTHYP +AGGREGATE = AGGREGAT +ARABIC = ARABISCH +ASIN = ARCSIN +ASINH = ARCSINHYP +ATAN = ARCTAN +ATAN2 = ARCTAN2 +ATANH = ARCTANHYP +BASE = BASIS +CEILING.MATH = OBERGRENZE.MATHEMATIK +CEILING.PRECISE = OBERGRENZE.GENAU +COMBIN = KOMBINATIONEN +COMBINA = KOMBINATIONEN2 +COS = COS +COSH = COSHYP +COT = COT +COTH = COTHYP +CSC = COSEC +CSCH = COSECHYP +DECIMAL = DEZIMAL +DEGREES = GRAD +ECMA.CEILING = ECMA.OBERGRENZE +EVEN = GERADE +EXP = EXP +FACT = FAKULTÄT +FACTDOUBLE = ZWEIFAKULTÄT +FLOOR.MATH = UNTERGRENZE.MATHEMATIK +FLOOR.PRECISE = UNTERGRENZE.GENAU +GCD = GGT +INT = GANZZAHL +ISO.CEILING = ISO.OBERGRENZE +LCM = KGV +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MDET +MINVERSE = MINV +MMULT = MMULT +MOD = REST +MROUND = VRUNDEN +MULTINOMIAL = POLYNOMIAL +MUNIT = MEINHEIT +ODD = UNGERADE +PI = PI +POWER = POTENZ +PRODUCT = PRODUKT +QUOTIENT = QUOTIENT +RADIANS = BOGENMASS +RAND = ZUFALLSZAHL +RANDBETWEEN = ZUFALLSBEREICH +ROMAN = RÖMISCH +ROUND = RUNDEN +ROUNDBAHTDOWN = RUNDBAHTNED +ROUNDBAHTUP = BAHTAUFRUNDEN +ROUNDDOWN = ABRUNDEN +ROUNDUP = AUFRUNDEN +SEC = SEC +SECH = SECHYP +SERIESSUM = POTENZREIHE +SIGN = VORZEICHEN +SIN = SIN +SINH = SINHYP +SQRT = WURZEL +SQRTPI = WURZELPI +SUBTOTAL = TEILERGEBNIS +SUM = SUMME +SUMIF = SUMMEWENN +SUMIFS = SUMMEWENNS +SUMPRODUCT = SUMMENPRODUKT +SUMSQ = QUADRATESUMME +SUMX2MY2 = SUMMEX2MY2 +SUMX2PY2 = SUMMEX2PY2 +SUMXMY2 = SUMMEXMY2 +TAN = TAN +TANH = TANHYP +TRUNC = KÜRZEN + +## +## Statistische Funktionen (Statistical Functions) +## +AVEDEV = MITTELABW +AVERAGE = MITTELWERT +AVERAGEA = MITTELWERTA +AVERAGEIF = MITTELWERTWENN +AVERAGEIFS = MITTELWERTWENNS +BETA.DIST = BETA.VERT +BETA.INV = BETA.INV +BINOM.DIST = BINOM.VERT +BINOM.DIST.RANGE = BINOM.VERT.BEREICH +BINOM.INV = BINOM.INV +CHISQ.DIST = CHIQU.VERT +CHISQ.DIST.RT = CHIQU.VERT.RE +CHISQ.INV = CHIQU.INV +CHISQ.INV.RT = CHIQU.INV.RE +CHISQ.TEST = CHIQU.TEST +CONFIDENCE.NORM = KONFIDENZ.NORM +CONFIDENCE.T = KONFIDENZ.T +CORREL = KORREL +COUNT = ANZAHL +COUNTA = ANZAHL2 +COUNTBLANK = ANZAHLLEEREZELLEN +COUNTIF = ZÄHLENWENN +COUNTIFS = ZÄHLENWENNS +COVARIANCE.P = KOVARIANZ.P +COVARIANCE.S = KOVARIANZ.S +DEVSQ = SUMQUADABW +EXPON.DIST = EXPON.VERT +F.DIST = F.VERT +F.DIST.RT = F.VERT.RE +F.INV = F.INV +F.INV.RT = F.INV.RE +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = PROGNOSE.ETS +FORECAST.ETS.CONFINT = PROGNOSE.ETS.KONFINT +FORECAST.ETS.SEASONALITY = PROGNOSE.ETS.SAISONALITÄT +FORECAST.ETS.STAT = PROGNOSE.ETS.STAT +FORECAST.LINEAR = PROGNOSE.LINEAR +FREQUENCY = HÄUFIGKEIT +GAMMA = GAMMA +GAMMA.DIST = GAMMA.VERT +GAMMA.INV = GAMMA.INV +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.GENAU +GAUSS = GAUSS +GEOMEAN = GEOMITTEL +GROWTH = VARIATION +HARMEAN = HARMITTEL +HYPGEOM.DIST = HYPGEOM.VERT +INTERCEPT = ACHSENABSCHNITT +KURT = KURT +LARGE = KGRÖSSTE +LINEST = RGP +LOGEST = RKP +LOGNORM.DIST = LOGNORM.VERT +LOGNORM.INV = LOGNORM.INV +MAX = MAX +MAXA = MAXA +MAXIFS = MAXWENNS +MEDIAN = MEDIAN +MIN = MIN +MINA = MINA +MINIFS = MINWENNS +MODE.MULT = MODUS.VIELF +MODE.SNGL = MODUS.EINF +NEGBINOM.DIST = NEGBINOM.VERT +NORM.DIST = NORM.VERT +NORM.INV = NORM.INV +NORM.S.DIST = NORM.S.VERT +NORM.S.INV = NORM.S.INV +PEARSON = PEARSON +PERCENTILE.EXC = QUANTIL.EXKL +PERCENTILE.INC = QUANTIL.INKL +PERCENTRANK.EXC = QUANTILSRANG.EXKL +PERCENTRANK.INC = QUANTILSRANG.INKL +PERMUT = VARIATIONEN +PERMUTATIONA = VARIATIONEN2 +PHI = PHI +POISSON.DIST = POISSON.VERT +PROB = WAHRSCHBEREICH +QUARTILE.EXC = QUARTILE.EXKL +QUARTILE.INC = QUARTILE.INKL +RANK.AVG = RANG.MITTELW +RANK.EQ = RANG.GLEICH +RSQ = BESTIMMTHEITSMASS +SKEW = SCHIEFE +SKEW.P = SCHIEFE.P +SLOPE = STEIGUNG +SMALL = KKLEINSTE +STANDARDIZE = STANDARDISIERUNG +STDEV.P = STABW.N +STDEV.S = STABW.S +STDEVA = STABWA +STDEVPA = STABWNA +STEYX = STFEHLERYX +T.DIST = T.VERT +T.DIST.2T = T.VERT.2S +T.DIST.RT = T.VERT.RE +T.INV = T.INV +T.INV.2T = T.INV.2S +T.TEST = T.TEST +TREND = TREND +TRIMMEAN = GESTUTZTMITTEL +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARIANZA +VARPA = VARIANZENA +WEIBULL.DIST = WEIBULL.VERT +Z.TEST = G.TEST + +## +## Textfunktionen (Text Functions) +## +BAHTTEXT = BAHTTEXT +CHAR = ZEICHEN +CLEAN = SÄUBERN +CODE = CODE +CONCAT = TEXTKETTE +DOLLAR = DM +EXACT = IDENTISCH +FIND = FINDEN +FIXED = FEST +ISTHAIDIGIT = ISTTHAIZAHLENWORT +LEFT = LINKS +LEN = LÄNGE +LOWER = KLEIN +MID = TEIL +NUMBERVALUE = ZAHLENWERT +PROPER = GROSS2 +REPLACE = ERSETZEN +REPT = WIEDERHOLEN +RIGHT = RECHTS +SEARCH = SUCHEN +SUBSTITUTE = WECHSELN +T = T +TEXT = TEXT +TEXTJOIN = TEXTVERKETTEN +THAIDIGIT = THAIZAHLENWORT +THAINUMSOUND = THAIZAHLSOUND +THAINUMSTRING = THAILANDSKNUMSTRENG +THAISTRINGLENGTH = THAIZEICHENFOLGENLÄNGE +TRIM = GLÄTTEN +UNICHAR = UNIZEICHEN +UNICODE = UNICODE +UPPER = GROSS +VALUE = WERT + +## +## Webfunktionen (Web Functions) +## +ENCODEURL = URLCODIEREN +FILTERXML = XMLFILTERN +WEBSERVICE = WEBDIENST + +## +## Kompatibilitätsfunktionen (Compatibility Functions) +## +BETADIST = BETAVERT +BETAINV = BETAINV +BINOMDIST = BINOMVERT +CEILING = OBERGRENZE +CHIDIST = CHIVERT +CHIINV = CHIINV +CHITEST = CHITEST +CONCATENATE = VERKETTEN +CONFIDENCE = KONFIDENZ +COVAR = KOVAR +CRITBINOM = KRITBINOM +EXPONDIST = EXPONVERT +FDIST = FVERT +FINV = FINV +FLOOR = UNTERGRENZE +FORECAST = SCHÄTZER +FTEST = FTEST +GAMMADIST = GAMMAVERT +GAMMAINV = GAMMAINV +HYPGEOMDIST = HYPGEOMVERT +LOGINV = LOGINV +LOGNORMDIST = LOGNORMVERT +MODE = MODALWERT +NEGBINOMDIST = NEGBINOMVERT +NORMDIST = NORMVERT +NORMINV = NORMINV +NORMSDIST = STANDNORMVERT +NORMSINV = STANDNORMINV +PERCENTILE = QUANTIL +PERCENTRANK = QUANTILSRANG +POISSON = POISSON +QUARTILE = QUARTILE +RANK = RANG +STDEV = STABW +STDEVP = STABWN +TDIST = TVERT +TINV = TINV +TTEST = TTEST +VAR = VARIANZ +VARP = VARIANZEN +WEIBULL = WEIBULL +ZTEST = GTEST diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/en/uk/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/en/uk/config new file mode 100644 index 0000000..4e068ab --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/en/uk/config @@ -0,0 +1,24 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## English-UK (English-UK) +## +############################################################ + +ArgumentSeparator = , +## +## (For future use) +## +currencySymbol = £ + +## +## Error Codes +## +NULL +DIV0 +VALUE +REF +NAME +NUM +NA diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/es/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/es/config new file mode 100644 index 0000000..fe044ef --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/es/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Español (Spanish) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #¡NULO! +DIV0 = #¡DIV/0! +VALUE = #¡VALOR! +REF = #¡REF! +NAME = #¿NOMBRE? +NUM = #¡NUM! +NA diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/es/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/es/functions new file mode 100644 index 0000000..88012aa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/es/functions @@ -0,0 +1,538 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Español (Spanish) +## +############################################################ + + +## +## Funciones de cubo (Cube Functions) +## +CUBEKPIMEMBER = MIEMBROKPICUBO +CUBEMEMBER = MIEMBROCUBO +CUBEMEMBERPROPERTY = PROPIEDADMIEMBROCUBO +CUBERANKEDMEMBER = MIEMBRORANGOCUBO +CUBESET = CONJUNTOCUBO +CUBESETCOUNT = RECUENTOCONJUNTOCUBO +CUBEVALUE = VALORCUBO + +## +## Funciones de base de datos (Database Functions) +## +DAVERAGE = BDPROMEDIO +DCOUNT = BDCONTAR +DCOUNTA = BDCONTARA +DGET = BDEXTRAER +DMAX = BDMAX +DMIN = BDMIN +DPRODUCT = BDPRODUCTO +DSTDEV = BDDESVEST +DSTDEVP = BDDESVESTP +DSUM = BDSUMA +DVAR = BDVAR +DVARP = BDVARP + +## +## Funciones de fecha y hora (Date & Time Functions) +## +DATE = FECHA +DATEDIF = SIFECHA +DATESTRING = CADENA.FECHA +DATEVALUE = FECHANUMERO +DAY = DIA +DAYS = DIAS +DAYS360 = DIAS360 +EDATE = FECHA.MES +EOMONTH = FIN.MES +HOUR = HORA +ISOWEEKNUM = ISO.NUM.DE.SEMANA +MINUTE = MINUTO +MONTH = MES +NETWORKDAYS = DIAS.LAB +NETWORKDAYS.INTL = DIAS.LAB.INTL +NOW = AHORA +SECOND = SEGUNDO +THAIDAYOFWEEK = DIASEMTAI +THAIMONTHOFYEAR = MESAÑOTAI +THAIYEAR = AÑOTAI +TIME = NSHORA +TIMEVALUE = HORANUMERO +TODAY = HOY +WEEKDAY = DIASEM +WEEKNUM = NUM.DE.SEMANA +WORKDAY = DIA.LAB +WORKDAY.INTL = DIA.LAB.INTL +YEAR = AÑO +YEARFRAC = FRAC.AÑO + +## +## Funciones de ingeniería (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BIN.A.DEC +BIN2HEX = BIN.A.HEX +BIN2OCT = BIN.A.OCT +BITAND = BIT.Y +BITLSHIFT = BIT.DESPLIZQDA +BITOR = BIT.O +BITRSHIFT = BIT.DESPLDCHA +BITXOR = BIT.XO +COMPLEX = COMPLEJO +CONVERT = CONVERTIR +DEC2BIN = DEC.A.BIN +DEC2HEX = DEC.A.HEX +DEC2OCT = DEC.A.OCT +DELTA = DELTA +ERF = FUN.ERROR +ERF.PRECISE = FUN.ERROR.EXACTO +ERFC = FUN.ERROR.COMPL +ERFC.PRECISE = FUN.ERROR.COMPL.EXACTO +GESTEP = MAYOR.O.IGUAL +HEX2BIN = HEX.A.BIN +HEX2DEC = HEX.A.DEC +HEX2OCT = HEX.A.OCT +IMABS = IM.ABS +IMAGINARY = IMAGINARIO +IMARGUMENT = IM.ANGULO +IMCONJUGATE = IM.CONJUGADA +IMCOS = IM.COS +IMCOSH = IM.COSH +IMCOT = IM.COT +IMCSC = IM.CSC +IMCSCH = IM.CSCH +IMDIV = IM.DIV +IMEXP = IM.EXP +IMLN = IM.LN +IMLOG10 = IM.LOG10 +IMLOG2 = IM.LOG2 +IMPOWER = IM.POT +IMPRODUCT = IM.PRODUCT +IMREAL = IM.REAL +IMSEC = IM.SEC +IMSECH = IM.SECH +IMSIN = IM.SENO +IMSINH = IM.SENOH +IMSQRT = IM.RAIZ2 +IMSUB = IM.SUSTR +IMSUM = IM.SUM +IMTAN = IM.TAN +OCT2BIN = OCT.A.BIN +OCT2DEC = OCT.A.DEC +OCT2HEX = OCT.A.HEX + +## +## Funciones financieras (Financial Functions) +## +ACCRINT = INT.ACUM +ACCRINTM = INT.ACUM.V +AMORDEGRC = AMORTIZ.PROGRE +AMORLINC = AMORTIZ.LIN +COUPDAYBS = CUPON.DIAS.L1 +COUPDAYS = CUPON.DIAS +COUPDAYSNC = CUPON.DIAS.L2 +COUPNCD = CUPON.FECHA.L2 +COUPNUM = CUPON.NUM +COUPPCD = CUPON.FECHA.L1 +CUMIPMT = PAGO.INT.ENTRE +CUMPRINC = PAGO.PRINC.ENTRE +DB = DB +DDB = DDB +DISC = TASA.DESC +DOLLARDE = MONEDA.DEC +DOLLARFR = MONEDA.FRAC +DURATION = DURACION +EFFECT = INT.EFECTIVO +FV = VF +FVSCHEDULE = VF.PLAN +INTRATE = TASA.INT +IPMT = PAGOINT +IRR = TIR +ISPMT = INT.PAGO.DIR +MDURATION = DURACION.MODIF +MIRR = TIRM +NOMINAL = TASA.NOMINAL +NPER = NPER +NPV = VNA +ODDFPRICE = PRECIO.PER.IRREGULAR.1 +ODDFYIELD = RENDTO.PER.IRREGULAR.1 +ODDLPRICE = PRECIO.PER.IRREGULAR.2 +ODDLYIELD = RENDTO.PER.IRREGULAR.2 +PDURATION = P.DURACION +PMT = PAGO +PPMT = PAGOPRIN +PRICE = PRECIO +PRICEDISC = PRECIO.DESCUENTO +PRICEMAT = PRECIO.VENCIMIENTO +PV = VA +RATE = TASA +RECEIVED = CANTIDAD.RECIBIDA +RRI = RRI +SLN = SLN +SYD = SYD +TBILLEQ = LETRA.DE.TEST.EQV.A.BONO +TBILLPRICE = LETRA.DE.TES.PRECIO +TBILLYIELD = LETRA.DE.TES.RENDTO +VDB = DVS +XIRR = TIR.NO.PER +XNPV = VNA.NO.PER +YIELD = RENDTO +YIELDDISC = RENDTO.DESC +YIELDMAT = RENDTO.VENCTO + +## +## Funciones de información (Information Functions) +## +CELL = CELDA +ERROR.TYPE = TIPO.DE.ERROR +INFO = INFO +ISBLANK = ESBLANCO +ISERR = ESERR +ISERROR = ESERROR +ISEVEN = ES.PAR +ISFORMULA = ESFORMULA +ISLOGICAL = ESLOGICO +ISNA = ESNOD +ISNONTEXT = ESNOTEXTO +ISNUMBER = ESNUMERO +ISODD = ES.IMPAR +ISREF = ESREF +ISTEXT = ESTEXTO +N = N +NA = NOD +SHEET = HOJA +SHEETS = HOJAS +TYPE = TIPO + +## +## Funciones lógicas (Logical Functions) +## +AND = Y +FALSE = FALSO +IF = SI +IFERROR = SI.ERROR +IFNA = SI.ND +IFS = SI.CONJUNTO +NOT = NO +OR = O +SWITCH = CAMBIAR +TRUE = VERDADERO +XOR = XO + +## +## Funciones de búsqueda y referencia (Lookup & Reference Functions) +## +ADDRESS = DIRECCION +AREAS = AREAS +CHOOSE = ELEGIR +COLUMN = COLUMNA +COLUMNS = COLUMNAS +FORMULATEXT = FORMULATEXTO +GETPIVOTDATA = IMPORTARDATOSDINAMICOS +HLOOKUP = BUSCARH +HYPERLINK = HIPERVINCULO +INDEX = INDICE +INDIRECT = INDIRECTO +LOOKUP = BUSCAR +MATCH = COINCIDIR +OFFSET = DESREF +ROW = FILA +ROWS = FILAS +RTD = RDTR +TRANSPOSE = TRANSPONER +VLOOKUP = BUSCARV +*RC = FC + +## +## Funciones matemáticas y trigonométricas (Math & Trig Functions) +## +ABS = ABS +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = AGREGAR +ARABIC = NUMERO.ARABE +ASIN = ASENO +ASINH = ASENOH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = BASE +CEILING.MATH = MULTIPLO.SUPERIOR.MAT +CEILING.PRECISE = MULTIPLO.SUPERIOR.EXACTO +COMBIN = COMBINAT +COMBINA = COMBINA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = CONV.DECIMAL +DEGREES = GRADOS +ECMA.CEILING = MULTIPLO.SUPERIOR.ECMA +EVEN = REDONDEA.PAR +EXP = EXP +FACT = FACT +FACTDOUBLE = FACT.DOBLE +FLOOR.MATH = MULTIPLO.INFERIOR.MAT +FLOOR.PRECISE = MULTIPLO.INFERIOR.EXACTO +GCD = M.C.D +INT = ENTERO +ISO.CEILING = MULTIPLO.SUPERIOR.ISO +LCM = M.C.M +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MDETERM +MINVERSE = MINVERSA +MMULT = MMULT +MOD = RESIDUO +MROUND = REDOND.MULT +MULTINOMIAL = MULTINOMIAL +MUNIT = M.UNIDAD +ODD = REDONDEA.IMPAR +PI = PI +POWER = POTENCIA +PRODUCT = PRODUCTO +QUOTIENT = COCIENTE +RADIANS = RADIANES +RAND = ALEATORIO +RANDBETWEEN = ALEATORIO.ENTRE +ROMAN = NUMERO.ROMANO +ROUND = REDONDEAR +ROUNDBAHTDOWN = REDONDEAR.BAHT.MAS +ROUNDBAHTUP = REDONDEAR.BAHT.MENOS +ROUNDDOWN = REDONDEAR.MENOS +ROUNDUP = REDONDEAR.MAS +SEC = SEC +SECH = SECH +SERIESSUM = SUMA.SERIES +SIGN = SIGNO +SIN = SENO +SINH = SENOH +SQRT = RAIZ +SQRTPI = RAIZ2PI +SUBTOTAL = SUBTOTALES +SUM = SUMA +SUMIF = SUMAR.SI +SUMIFS = SUMAR.SI.CONJUNTO +SUMPRODUCT = SUMAPRODUCTO +SUMSQ = SUMA.CUADRADOS +SUMX2MY2 = SUMAX2MENOSY2 +SUMX2PY2 = SUMAX2MASY2 +SUMXMY2 = SUMAXMENOSY2 +TAN = TAN +TANH = TANH +TRUNC = TRUNCAR + +## +## Funciones estadísticas (Statistical Functions) +## +AVEDEV = DESVPROM +AVERAGE = PROMEDIO +AVERAGEA = PROMEDIOA +AVERAGEIF = PROMEDIO.SI +AVERAGEIFS = PROMEDIO.SI.CONJUNTO +BETA.DIST = DISTR.BETA.N +BETA.INV = INV.BETA.N +BINOM.DIST = DISTR.BINOM.N +BINOM.DIST.RANGE = DISTR.BINOM.SERIE +BINOM.INV = INV.BINOM +CHISQ.DIST = DISTR.CHICUAD +CHISQ.DIST.RT = DISTR.CHICUAD.CD +CHISQ.INV = INV.CHICUAD +CHISQ.INV.RT = INV.CHICUAD.CD +CHISQ.TEST = PRUEBA.CHICUAD +CONFIDENCE.NORM = INTERVALO.CONFIANZA.NORM +CONFIDENCE.T = INTERVALO.CONFIANZA.T +CORREL = COEF.DE.CORREL +COUNT = CONTAR +COUNTA = CONTARA +COUNTBLANK = CONTAR.BLANCO +COUNTIF = CONTAR.SI +COUNTIFS = CONTAR.SI.CONJUNTO +COVARIANCE.P = COVARIANCE.P +COVARIANCE.S = COVARIANZA.M +DEVSQ = DESVIA2 +EXPON.DIST = DISTR.EXP.N +F.DIST = DISTR.F.N +F.DIST.RT = DISTR.F.CD +F.INV = INV.F +F.INV.RT = INV.F.CD +F.TEST = PRUEBA.F.N +FISHER = FISHER +FISHERINV = PRUEBA.FISHER.INV +FORECAST.ETS = PRONOSTICO.ETS +FORECAST.ETS.CONFINT = PRONOSTICO.ETS.CONFINT +FORECAST.ETS.SEASONALITY = PRONOSTICO.ETS.ESTACIONALIDAD +FORECAST.ETS.STAT = PRONOSTICO.ETS.STAT +FORECAST.LINEAR = PRONOSTICO.LINEAL +FREQUENCY = FRECUENCIA +GAMMA = GAMMA +GAMMA.DIST = DISTR.GAMMA.N +GAMMA.INV = INV.GAMMA +GAMMALN = GAMMA.LN +GAMMALN.PRECISE = GAMMA.LN.EXACTO +GAUSS = GAUSS +GEOMEAN = MEDIA.GEOM +GROWTH = CRECIMIENTO +HARMEAN = MEDIA.ARMO +HYPGEOM.DIST = DISTR.HIPERGEOM.N +INTERCEPT = INTERSECCION.EJE +KURT = CURTOSIS +LARGE = K.ESIMO.MAYOR +LINEST = ESTIMACION.LINEAL +LOGEST = ESTIMACION.LOGARITMICA +LOGNORM.DIST = DISTR.LOGNORM +LOGNORM.INV = INV.LOGNORM +MAX = MAX +MAXA = MAXA +MAXIFS = MAX.SI.CONJUNTO +MEDIAN = MEDIANA +MIN = MIN +MINA = MINA +MINIFS = MIN.SI.CONJUNTO +MODE.MULT = MODA.VARIOS +MODE.SNGL = MODA.UNO +NEGBINOM.DIST = NEGBINOM.DIST +NORM.DIST = DISTR.NORM.N +NORM.INV = INV.NORM +NORM.S.DIST = DISTR.NORM.ESTAND.N +NORM.S.INV = INV.NORM.ESTAND +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTIL.EXC +PERCENTILE.INC = PERCENTIL.INC +PERCENTRANK.EXC = RANGO.PERCENTIL.EXC +PERCENTRANK.INC = RANGO.PERCENTIL.INC +PERMUT = PERMUTACIONES +PERMUTATIONA = PERMUTACIONES.A +PHI = FI +POISSON.DIST = POISSON.DIST +PROB = PROBABILIDAD +QUARTILE.EXC = CUARTIL.EXC +QUARTILE.INC = CUARTIL.INC +RANK.AVG = JERARQUIA.MEDIA +RANK.EQ = JERARQUIA.EQV +RSQ = COEFICIENTE.R2 +SKEW = COEFICIENTE.ASIMETRIA +SKEW.P = COEFICIENTE.ASIMETRIA.P +SLOPE = PENDIENTE +SMALL = K.ESIMO.MENOR +STANDARDIZE = NORMALIZACION +STDEV.P = DESVEST.P +STDEV.S = DESVEST.M +STDEVA = DESVESTA +STDEVPA = DESVESTPA +STEYX = ERROR.TIPICO.XY +T.DIST = DISTR.T.N +T.DIST.2T = DISTR.T.2C +T.DIST.RT = DISTR.T.CD +T.INV = INV.T +T.INV.2T = INV.T.2C +T.TEST = PRUEBA.T.N +TREND = TENDENCIA +TRIMMEAN = MEDIA.ACOTADA +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = DISTR.WEIBULL +Z.TEST = PRUEBA.Z.N + +## +## Funciones de texto (Text Functions) +## +BAHTTEXT = TEXTOBAHT +CHAR = CARACTER +CLEAN = LIMPIAR +CODE = CODIGO +CONCAT = CONCAT +DOLLAR = MONEDA +EXACT = IGUAL +FIND = ENCONTRAR +FIXED = DECIMAL +ISTHAIDIGIT = ESDIGITOTAI +LEFT = IZQUIERDA +LEN = LARGO +LOWER = MINUSC +MID = EXTRAE +NUMBERSTRING = CADENA.NUMERO +NUMBERVALUE = VALOR.NUMERO +PHONETIC = FONETICO +PROPER = NOMPROPIO +REPLACE = REEMPLAZAR +REPT = REPETIR +RIGHT = DERECHA +SEARCH = HALLAR +SUBSTITUTE = SUSTITUIR +T = T +TEXT = TEXTO +TEXTJOIN = UNIRCADENAS +THAIDIGIT = DIGITOTAI +THAINUMSOUND = SONNUMTAI +THAINUMSTRING = CADENANUMTAI +THAISTRINGLENGTH = LONGCADENATAI +TRIM = ESPACIOS +UNICHAR = UNICAR +UNICODE = UNICODE +UPPER = MAYUSC +VALUE = VALOR + +## +## Funciones web (Web Functions) +## +ENCODEURL = URLCODIF +FILTERXML = XMLFILTRO +WEBSERVICE = SERVICIOWEB + +## +## Funciones de compatibilidad (Compatibility Functions) +## +BETADIST = DISTR.BETA +BETAINV = DISTR.BETA.INV +BINOMDIST = DISTR.BINOM +CEILING = MULTIPLO.SUPERIOR +CHIDIST = DISTR.CHI +CHIINV = PRUEBA.CHI.INV +CHITEST = PRUEBA.CHI +CONCATENATE = CONCATENAR +CONFIDENCE = INTERVALO.CONFIANZA +COVAR = COVAR +CRITBINOM = BINOM.CRIT +EXPONDIST = DISTR.EXP +FDIST = DISTR.F +FINV = DISTR.F.INV +FLOOR = MULTIPLO.INFERIOR +FORECAST = PRONOSTICO +FTEST = PRUEBA.F +GAMMADIST = DISTR.GAMMA +GAMMAINV = DISTR.GAMMA.INV +HYPGEOMDIST = DISTR.HIPERGEOM +LOGINV = DISTR.LOG.INV +LOGNORMDIST = DISTR.LOG.NORM +MODE = MODA +NEGBINOMDIST = NEGBINOMDIST +NORMDIST = DISTR.NORM +NORMINV = DISTR.NORM.INV +NORMSDIST = DISTR.NORM.ESTAND +NORMSINV = DISTR.NORM.ESTAND.INV +PERCENTILE = PERCENTIL +PERCENTRANK = RANGO.PERCENTIL +POISSON = POISSON +QUARTILE = CUARTIL +RANK = JERARQUIA +STDEV = DESVEST +STDEVP = DESVESTP +TDIST = DISTR.T +TINV = DISTR.T.INV +TTEST = PRUEBA.T +VAR = VAR +VARP = VARP +WEIBULL = DIST.WEIBULL +ZTEST = PRUEBA.Z diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fi/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fi/config new file mode 100644 index 0000000..5388f93 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fi/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Suomi (Finnish) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #TYHJÄ! +DIV0 = #JAKO/0! +VALUE = #ARVO! +REF = #VIITTAUS! +NAME = #NIMI? +NUM = #LUKU! +NA = #PUUTTUU! diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fi/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fi/functions new file mode 100644 index 0000000..18f7c8c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fi/functions @@ -0,0 +1,538 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Suomi (Finnish) +## +############################################################ + + +## +## Kuutiofunktiot (Cube Functions) +## +CUBEKPIMEMBER = KUUTIOKPIJÄSEN +CUBEMEMBER = KUUTIONJÄSEN +CUBEMEMBERPROPERTY = KUUTIONJÄSENENOMINAISUUS +CUBERANKEDMEMBER = KUUTIONLUOKITELTUJÄSEN +CUBESET = KUUTIOJOUKKO +CUBESETCOUNT = KUUTIOJOUKKOJENMÄÄRÄ +CUBEVALUE = KUUTIONARVO + +## +## Tietokantafunktiot (Database Functions) +## +DAVERAGE = TKESKIARVO +DCOUNT = TLASKE +DCOUNTA = TLASKEA +DGET = TNOUDA +DMAX = TMAKS +DMIN = TMIN +DPRODUCT = TTULO +DSTDEV = TKESKIHAJONTA +DSTDEVP = TKESKIHAJONTAP +DSUM = TSUMMA +DVAR = TVARIANSSI +DVARP = TVARIANSSIP + +## +## Päivämäärä- ja aikafunktiot (Date & Time Functions) +## +DATE = PÄIVÄYS +DATEDIF = PVMERO +DATESTRING = PVMMERKKIJONO +DATEVALUE = PÄIVÄYSARVO +DAY = PÄIVÄ +DAYS = PÄIVÄT +DAYS360 = PÄIVÄT360 +EDATE = PÄIVÄ.KUUKAUSI +EOMONTH = KUUKAUSI.LOPPU +HOUR = TUNNIT +ISOWEEKNUM = VIIKKO.ISO.NRO +MINUTE = MINUUTIT +MONTH = KUUKAUSI +NETWORKDAYS = TYÖPÄIVÄT +NETWORKDAYS.INTL = TYÖPÄIVÄT.KANSVÄL +NOW = NYT +SECOND = SEKUNNIT +THAIDAYOFWEEK = THAI.VIIKONPÄIVÄ +THAIMONTHOFYEAR = THAI.KUUKAUSI +THAIYEAR = THAI.VUOSI +TIME = AIKA +TIMEVALUE = AIKA_ARVO +TODAY = TÄMÄ.PÄIVÄ +WEEKDAY = VIIKONPÄIVÄ +WEEKNUM = VIIKKO.NRO +WORKDAY = TYÖPÄIVÄ +WORKDAY.INTL = TYÖPÄIVÄ.KANSVÄL +YEAR = VUOSI +YEARFRAC = VUOSI.OSA + +## +## Tekniset funktiot (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BINDES +BIN2HEX = BINHEKSA +BIN2OCT = BINOKT +BITAND = BITTI.JA +BITLSHIFT = BITTI.SIIRTO.V +BITOR = BITTI.TAI +BITRSHIFT = BITTI.SIIRTO.O +BITXOR = BITTI.EHDOTON.TAI +COMPLEX = KOMPLEKSI +CONVERT = MUUNNA +DEC2BIN = DESBIN +DEC2HEX = DESHEKSA +DEC2OCT = DESOKT +DELTA = SAMA.ARVO +ERF = VIRHEFUNKTIO +ERF.PRECISE = VIRHEFUNKTIO.TARKKA +ERFC = VIRHEFUNKTIO.KOMPLEMENTTI +ERFC.PRECISE = VIRHEFUNKTIO.KOMPLEMENTTI.TARKKA +GESTEP = RAJA +HEX2BIN = HEKSABIN +HEX2DEC = HEKSADES +HEX2OCT = HEKSAOKT +IMABS = KOMPLEKSI.ABS +IMAGINARY = KOMPLEKSI.IMAG +IMARGUMENT = KOMPLEKSI.ARG +IMCONJUGATE = KOMPLEKSI.KONJ +IMCOS = KOMPLEKSI.COS +IMCOSH = IMCOSH +IMCOT = KOMPLEKSI.COT +IMCSC = KOMPLEKSI.KOSEK +IMCSCH = KOMPLEKSI.KOSEKH +IMDIV = KOMPLEKSI.OSAM +IMEXP = KOMPLEKSI.EKSP +IMLN = KOMPLEKSI.LN +IMLOG10 = KOMPLEKSI.LOG10 +IMLOG2 = KOMPLEKSI.LOG2 +IMPOWER = KOMPLEKSI.POT +IMPRODUCT = KOMPLEKSI.TULO +IMREAL = KOMPLEKSI.REAALI +IMSEC = KOMPLEKSI.SEK +IMSECH = KOMPLEKSI.SEKH +IMSIN = KOMPLEKSI.SIN +IMSINH = KOMPLEKSI.SINH +IMSQRT = KOMPLEKSI.NELIÖJ +IMSUB = KOMPLEKSI.EROTUS +IMSUM = KOMPLEKSI.SUM +IMTAN = KOMPLEKSI.TAN +OCT2BIN = OKTBIN +OCT2DEC = OKTDES +OCT2HEX = OKTHEKSA + +## +## Rahoitusfunktiot (Financial Functions) +## +ACCRINT = KERTYNYT.KORKO +ACCRINTM = KERTYNYT.KORKO.LOPUSSA +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = KORKOPÄIVÄT.ALUSTA +COUPDAYS = KORKOPÄIVÄT +COUPDAYSNC = KORKOPÄIVÄT.SEURAAVA +COUPNCD = KORKOPÄIVÄ.SEURAAVA +COUPNUM = KORKOPÄIVÄ.JAKSOT +COUPPCD = KORKOPÄIVÄ.EDELLINEN +CUMIPMT = MAKSETTU.KORKO +CUMPRINC = MAKSETTU.LYHENNYS +DB = DB +DDB = DDB +DISC = DISKONTTOKORKO +DOLLARDE = VALUUTTA.DES +DOLLARFR = VALUUTTA.MURTO +DURATION = KESTO +EFFECT = KORKO.EFEKT +FV = TULEVA.ARVO +FVSCHEDULE = TULEVA.ARVO.ERIKORKO +INTRATE = KORKO.ARVOPAPERI +IPMT = IPMT +IRR = SISÄINEN.KORKO +ISPMT = ISPMT +MDURATION = KESTO.MUUNN +MIRR = MSISÄINEN +NOMINAL = KORKO.VUOSI +NPER = NJAKSO +NPV = NNA +ODDFPRICE = PARITON.ENS.NIMELLISARVO +ODDFYIELD = PARITON.ENS.TUOTTO +ODDLPRICE = PARITON.VIIM.NIMELLISARVO +ODDLYIELD = PARITON.VIIM.TUOTTO +PDURATION = KESTO.JAKSO +PMT = MAKSU +PPMT = PPMT +PRICE = HINTA +PRICEDISC = HINTA.DISK +PRICEMAT = HINTA.LUNASTUS +PV = NA +RATE = KORKO +RECEIVED = SAATU.HINTA +RRI = TOT.ROI +SLN = STP +SYD = VUOSIPOISTO +TBILLEQ = OBLIG.TUOTTOPROS +TBILLPRICE = OBLIG.HINTA +TBILLYIELD = OBLIG.TUOTTO +VDB = VDB +XIRR = SISÄINEN.KORKO.JAKSOTON +XNPV = NNA.JAKSOTON +YIELD = TUOTTO +YIELDDISC = TUOTTO.DISK +YIELDMAT = TUOTTO.ERÄP + +## +## Tietofunktiot (Information Functions) +## +CELL = SOLU +ERROR.TYPE = VIRHEEN.LAJI +INFO = KUVAUS +ISBLANK = ONTYHJÄ +ISERR = ONVIRH +ISERROR = ONVIRHE +ISEVEN = ONPARILLINEN +ISFORMULA = ONKAAVA +ISLOGICAL = ONTOTUUS +ISNA = ONPUUTTUU +ISNONTEXT = ONEI_TEKSTI +ISNUMBER = ONLUKU +ISODD = ONPARITON +ISREF = ONVIITT +ISTEXT = ONTEKSTI +N = N +NA = PUUTTUU +SHEET = TAULUKKO +SHEETS = TAULUKOT +TYPE = TYYPPI + +## +## Loogiset funktiot (Logical Functions) +## +AND = JA +FALSE = EPÄTOSI +IF = JOS +IFERROR = JOSVIRHE +IFNA = JOSPUUTTUU +IFS = JOSS +NOT = EI +OR = TAI +SWITCH = MUUTA +TRUE = TOSI +XOR = EHDOTON.TAI + +## +## Haku- ja viitefunktiot (Lookup & Reference Functions) +## +ADDRESS = OSOITE +AREAS = ALUEET +CHOOSE = VALITSE.INDEKSI +COLUMN = SARAKE +COLUMNS = SARAKKEET +FORMULATEXT = KAAVA.TEKSTI +GETPIVOTDATA = NOUDA.PIVOT.TIEDOT +HLOOKUP = VHAKU +HYPERLINK = HYPERLINKKI +INDEX = INDEKSI +INDIRECT = EPÄSUORA +LOOKUP = HAKU +MATCH = VASTINE +OFFSET = SIIRTYMÄ +ROW = RIVI +ROWS = RIVIT +RTD = RTD +TRANSPOSE = TRANSPONOI +VLOOKUP = PHAKU +*RC = RS + +## +## Matemaattiset ja trigonometriset funktiot (Math & Trig Functions) +## +ABS = ITSEISARVO +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = KOOSTE +ARABIC = ARABIA +ASIN = ASIN +ASINH = ASINH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = PERUS +CEILING.MATH = PYÖRISTÄ.KERR.YLÖS.MATEMAATTINEN +CEILING.PRECISE = PYÖRISTÄ.KERR.YLÖS.TARKKA +COMBIN = KOMBINAATIO +COMBINA = KOMBINAATIOA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = KOSEK +CSCH = KOSEKH +DECIMAL = DESIMAALI +DEGREES = ASTEET +ECMA.CEILING = ECMA.PYÖRISTÄ.KERR.YLÖS +EVEN = PARILLINEN +EXP = EKSPONENTTI +FACT = KERTOMA +FACTDOUBLE = KERTOMA.OSA +FLOOR.MATH = PYÖRISTÄ.KERR.ALAS.MATEMAATTINEN +FLOOR.PRECISE = PYÖRISTÄ.KERR.ALAS.TARKKA +GCD = SUURIN.YHT.TEKIJÄ +INT = KOKONAISLUKU +ISO.CEILING = ISO.PYÖRISTÄ.KERR.YLÖS +LCM = PIENIN.YHT.JAETTAVA +LN = LUONNLOG +LOG = LOG +LOG10 = LOG10 +MDETERM = MDETERM +MINVERSE = MKÄÄNTEINEN +MMULT = MKERRO +MOD = JAKOJ +MROUND = PYÖRISTÄ.KERR +MULTINOMIAL = MULTINOMI +MUNIT = YKSIKKÖM +ODD = PARITON +PI = PII +POWER = POTENSSI +PRODUCT = TULO +QUOTIENT = OSAMÄÄRÄ +RADIANS = RADIAANIT +RAND = SATUNNAISLUKU +RANDBETWEEN = SATUNNAISLUKU.VÄLILTÄ +ROMAN = ROMAN +ROUND = PYÖRISTÄ +ROUNDBAHTDOWN = PYÖRISTÄ.BAHT.ALAS +ROUNDBAHTUP = PYÖRISTÄ.BAHT.YLÖS +ROUNDDOWN = PYÖRISTÄ.DES.ALAS +ROUNDUP = PYÖRISTÄ.DES.YLÖS +SEC = SEK +SECH = SEKH +SERIESSUM = SARJA.SUMMA +SIGN = ETUMERKKI +SIN = SIN +SINH = SINH +SQRT = NELIÖJUURI +SQRTPI = NELIÖJUURI.PII +SUBTOTAL = VÄLISUMMA +SUM = SUMMA +SUMIF = SUMMA.JOS +SUMIFS = SUMMA.JOS.JOUKKO +SUMPRODUCT = TULOJEN.SUMMA +SUMSQ = NELIÖSUMMA +SUMX2MY2 = NELIÖSUMMIEN.EROTUS +SUMX2PY2 = NELIÖSUMMIEN.SUMMA +SUMXMY2 = EROTUSTEN.NELIÖSUMMA +TAN = TAN +TANH = TANH +TRUNC = KATKAISE + +## +## Tilastolliset funktiot (Statistical Functions) +## +AVEDEV = KESKIPOIKKEAMA +AVERAGE = KESKIARVO +AVERAGEA = KESKIARVOA +AVERAGEIF = KESKIARVO.JOS +AVERAGEIFS = KESKIARVO.JOS.JOUKKO +BETA.DIST = BEETA.JAKAUMA +BETA.INV = BEETA.KÄÄNT +BINOM.DIST = BINOMI.JAKAUMA +BINOM.DIST.RANGE = BINOMI.JAKAUMA.ALUE +BINOM.INV = BINOMIJAKAUMA.KÄÄNT +CHISQ.DIST = CHINELIÖ.JAKAUMA +CHISQ.DIST.RT = CHINELIÖ.JAKAUMA.OH +CHISQ.INV = CHINELIÖ.KÄÄNT +CHISQ.INV.RT = CHINELIÖ.KÄÄNT.OH +CHISQ.TEST = CHINELIÖ.TESTI +CONFIDENCE.NORM = LUOTTAMUSVÄLI.NORM +CONFIDENCE.T = LUOTTAMUSVÄLI.T +CORREL = KORRELAATIO +COUNT = LASKE +COUNTA = LASKE.A +COUNTBLANK = LASKE.TYHJÄT +COUNTIF = LASKE.JOS +COUNTIFS = LASKE.JOS.JOUKKO +COVARIANCE.P = KOVARIANSSI.P +COVARIANCE.S = KOVARIANSSI.S +DEVSQ = OIKAISTU.NELIÖSUMMA +EXPON.DIST = EKSPONENTIAALI.JAKAUMA +F.DIST = F.JAKAUMA +F.DIST.RT = F.JAKAUMA.OH +F.INV = F.KÄÄNT +F.INV.RT = F.KÄÄNT.OH +F.TEST = F.TESTI +FISHER = FISHER +FISHERINV = FISHER.KÄÄNT +FORECAST.ETS = ENNUSTE.ETS +FORECAST.ETS.CONFINT = ENNUSTE.ETS.CONFINT +FORECAST.ETS.SEASONALITY = ENNUSTE.ETS.KAUSIVAIHTELU +FORECAST.ETS.STAT = ENNUSTE.ETS.STAT +FORECAST.LINEAR = ENNUSTE.LINEAARINEN +FREQUENCY = TAAJUUS +GAMMA = GAMMA +GAMMA.DIST = GAMMA.JAKAUMA +GAMMA.INV = GAMMA.JAKAUMA.KÄÄNT +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.TARKKA +GAUSS = GAUSS +GEOMEAN = KESKIARVO.GEOM +GROWTH = KASVU +HARMEAN = KESKIARVO.HARM +HYPGEOM.DIST = HYPERGEOM_JAKAUMA +INTERCEPT = LEIKKAUSPISTE +KURT = KURT +LARGE = SUURI +LINEST = LINREGR +LOGEST = LOGREGR +LOGNORM.DIST = LOGNORM_JAKAUMA +LOGNORM.INV = LOGNORM.KÄÄNT +MAX = MAKS +MAXA = MAKSA +MAXIFS = MAKS.JOS +MEDIAN = MEDIAANI +MIN = MIN +MINA = MINA +MINIFS = MIN.JOS +MODE.MULT = MOODI.USEA +MODE.SNGL = MOODI.YKSI +NEGBINOM.DIST = BINOMI.JAKAUMA.NEG +NORM.DIST = NORMAALI.JAKAUMA +NORM.INV = NORMAALI.JAKAUMA.KÄÄNT +NORM.S.DIST = NORM_JAKAUMA.NORMIT +NORM.S.INV = NORM_JAKAUMA.KÄÄNT +PEARSON = PEARSON +PERCENTILE.EXC = PROSENTTIPISTE.ULK +PERCENTILE.INC = PROSENTTIPISTE.SIS +PERCENTRANK.EXC = PROSENTTIJÄRJESTYS.ULK +PERCENTRANK.INC = PROSENTTIJÄRJESTYS.SIS +PERMUT = PERMUTAATIO +PERMUTATIONA = PERMUTAATIOA +PHI = FII +POISSON.DIST = POISSON.JAKAUMA +PROB = TODENNÄKÖISYYS +QUARTILE.EXC = NELJÄNNES.ULK +QUARTILE.INC = NELJÄNNES.SIS +RANK.AVG = ARVON.MUKAAN.KESKIARVO +RANK.EQ = ARVON.MUKAAN.TASAN +RSQ = PEARSON.NELIÖ +SKEW = JAKAUMAN.VINOUS +SKEW.P = JAKAUMAN.VINOUS.POP +SLOPE = KULMAKERROIN +SMALL = PIENI +STANDARDIZE = NORMITA +STDEV.P = KESKIHAJONTA.P +STDEV.S = KESKIHAJONTA.S +STDEVA = KESKIHAJONTAA +STDEVPA = KESKIHAJONTAPA +STEYX = KESKIVIRHE +T.DIST = T.JAKAUMA +T.DIST.2T = T.JAKAUMA.2S +T.DIST.RT = T.JAKAUMA.OH +T.INV = T.KÄÄNT +T.INV.2T = T.KÄÄNT.2S +T.TEST = T.TESTI +TREND = SUUNTAUS +TRIMMEAN = KESKIARVO.TASATTU +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = WEIBULL.JAKAUMA +Z.TEST = Z.TESTI + +## +## Tekstifunktiot (Text Functions) +## +BAHTTEXT = BAHTTEKSTI +CHAR = MERKKI +CLEAN = SIIVOA +CODE = KOODI +CONCAT = YHDISTÄ +DOLLAR = VALUUTTA +EXACT = VERTAA +FIND = ETSI +FIXED = KIINTEÄ +ISTHAIDIGIT = ON.THAI.NUMERO +LEFT = VASEN +LEN = PITUUS +LOWER = PIENET +MID = POIMI.TEKSTI +NUMBERSTRING = NROMERKKIJONO +NUMBERVALUE = NROARVO +PHONETIC = FONEETTINEN +PROPER = ERISNIMI +REPLACE = KORVAA +REPT = TOISTA +RIGHT = OIKEA +SEARCH = KÄY.LÄPI +SUBSTITUTE = VAIHDA +T = T +TEXT = TEKSTI +TEXTJOIN = TEKSTI.YHDISTÄ +THAIDIGIT = THAI.NUMERO +THAINUMSOUND = THAI.LUKU.ÄÄNI +THAINUMSTRING = THAI.LUKU.MERKKIJONO +THAISTRINGLENGTH = THAI.MERKKIJONON.PITUUS +TRIM = POISTA.VÄLIT +UNICHAR = UNICODEMERKKI +UNICODE = UNICODE +UPPER = ISOT +VALUE = ARVO + +## +## Verkkofunktiot (Web Functions) +## +ENCODEURL = URLKOODAUS +FILTERXML = SUODATA.XML +WEBSERVICE = VERKKOPALVELU + +## +## Yhteensopivuusfunktiot (Compatibility Functions) +## +BETADIST = BEETAJAKAUMA +BETAINV = BEETAJAKAUMA.KÄÄNT +BINOMDIST = BINOMIJAKAUMA +CEILING = PYÖRISTÄ.KERR.YLÖS +CHIDIST = CHIJAKAUMA +CHIINV = CHIJAKAUMA.KÄÄNT +CHITEST = CHITESTI +CONCATENATE = KETJUTA +CONFIDENCE = LUOTTAMUSVÄLI +COVAR = KOVARIANSSI +CRITBINOM = BINOMIJAKAUMA.KRIT +EXPONDIST = EKSPONENTIAALIJAKAUMA +FDIST = FJAKAUMA +FINV = FJAKAUMA.KÄÄNT +FLOOR = PYÖRISTÄ.KERR.ALAS +FORECAST = ENNUSTE +FTEST = FTESTI +GAMMADIST = GAMMAJAKAUMA +GAMMAINV = GAMMAJAKAUMA.KÄÄNT +HYPGEOMDIST = HYPERGEOM.JAKAUMA +LOGINV = LOGNORM.JAKAUMA.KÄÄNT +LOGNORMDIST = LOGNORM.JAKAUMA +MODE = MOODI +NEGBINOMDIST = BINOMIJAKAUMA.NEG +NORMDIST = NORM.JAKAUMA +NORMINV = NORM.JAKAUMA.KÄÄNT +NORMSDIST = NORM.JAKAUMA.NORMIT +NORMSINV = NORM.JAKAUMA.NORMIT.KÄÄNT +PERCENTILE = PROSENTTIPISTE +PERCENTRANK = PROSENTTIJÄRJESTYS +POISSON = POISSON +QUARTILE = NELJÄNNES +RANK = ARVON.MUKAAN +STDEV = KESKIHAJONTA +STDEVP = KESKIHAJONTAP +TDIST = TJAKAUMA +TINV = TJAKAUMA.KÄÄNT +TTEST = TTESTI +VAR = VAR +VARP = VARP +WEIBULL = WEIBULL +ZTEST = ZTESTI diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fr/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fr/config new file mode 100644 index 0000000..bdac412 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fr/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Français (French) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #NUL! +DIV0 +VALUE = #VALEUR! +REF +NAME = #NOM? +NUM = #NOMBRE! +NA diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fr/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fr/functions new file mode 100644 index 0000000..621cb0d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/fr/functions @@ -0,0 +1,525 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Français (French) +## +############################################################ + + +## +## Fonctions Cube (Cube Functions) +## +CUBEKPIMEMBER = MEMBREKPICUBE +CUBEMEMBER = MEMBRECUBE +CUBEMEMBERPROPERTY = PROPRIETEMEMBRECUBE +CUBERANKEDMEMBER = RANGMEMBRECUBE +CUBESET = JEUCUBE +CUBESETCOUNT = NBJEUCUBE +CUBEVALUE = VALEURCUBE + +## +## Fonctions de base de données (Database Functions) +## +DAVERAGE = BDMOYENNE +DCOUNT = BDNB +DCOUNTA = BDNBVAL +DGET = BDLIRE +DMAX = BDMAX +DMIN = BDMIN +DPRODUCT = BDPRODUIT +DSTDEV = BDECARTYPE +DSTDEVP = BDECARTYPEP +DSUM = BDSOMME +DVAR = BDVAR +DVARP = BDVARP + +## +## Fonctions de date et d’heure (Date & Time Functions) +## +DATE = DATE +DATEVALUE = DATEVAL +DAY = JOUR +DAYS = JOURS +DAYS360 = JOURS360 +EDATE = MOIS.DECALER +EOMONTH = FIN.MOIS +HOUR = HEURE +ISOWEEKNUM = NO.SEMAINE.ISO +MINUTE = MINUTE +MONTH = MOIS +NETWORKDAYS = NB.JOURS.OUVRES +NETWORKDAYS.INTL = NB.JOURS.OUVRES.INTL +NOW = MAINTENANT +SECOND = SECONDE +TIME = TEMPS +TIMEVALUE = TEMPSVAL +TODAY = AUJOURDHUI +WEEKDAY = JOURSEM +WEEKNUM = NO.SEMAINE +WORKDAY = SERIE.JOUR.OUVRE +WORKDAY.INTL = SERIE.JOUR.OUVRE.INTL +YEAR = ANNEE +YEARFRAC = FRACTION.ANNEE + +## +## Fonctions d’ingénierie (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BINDEC +BIN2HEX = BINHEX +BIN2OCT = BINOCT +BITAND = BITET +BITLSHIFT = BITDECALG +BITOR = BITOU +BITRSHIFT = BITDECALD +BITXOR = BITOUEXCLUSIF +COMPLEX = COMPLEXE +CONVERT = CONVERT +DEC2BIN = DECBIN +DEC2HEX = DECHEX +DEC2OCT = DECOCT +DELTA = DELTA +ERF = ERF +ERF.PRECISE = ERF.PRECIS +ERFC = ERFC +ERFC.PRECISE = ERFC.PRECIS +GESTEP = SUP.SEUIL +HEX2BIN = HEXBIN +HEX2DEC = HEXDEC +HEX2OCT = HEXOCT +IMABS = COMPLEXE.MODULE +IMAGINARY = COMPLEXE.IMAGINAIRE +IMARGUMENT = COMPLEXE.ARGUMENT +IMCONJUGATE = COMPLEXE.CONJUGUE +IMCOS = COMPLEXE.COS +IMCOSH = COMPLEXE.COSH +IMCOT = COMPLEXE.COT +IMCSC = COMPLEXE.CSC +IMCSCH = COMPLEXE.CSCH +IMDIV = COMPLEXE.DIV +IMEXP = COMPLEXE.EXP +IMLN = COMPLEXE.LN +IMLOG10 = COMPLEXE.LOG10 +IMLOG2 = COMPLEXE.LOG2 +IMPOWER = COMPLEXE.PUISSANCE +IMPRODUCT = COMPLEXE.PRODUIT +IMREAL = COMPLEXE.REEL +IMSEC = COMPLEXE.SEC +IMSECH = COMPLEXE.SECH +IMSIN = COMPLEXE.SIN +IMSINH = COMPLEXE.SINH +IMSQRT = COMPLEXE.RACINE +IMSUB = COMPLEXE.DIFFERENCE +IMSUM = COMPLEXE.SOMME +IMTAN = COMPLEXE.TAN +OCT2BIN = OCTBIN +OCT2DEC = OCTDEC +OCT2HEX = OCTHEX + +## +## Fonctions financières (Financial Functions) +## +ACCRINT = INTERET.ACC +ACCRINTM = INTERET.ACC.MAT +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = NB.JOURS.COUPON.PREC +COUPDAYS = NB.JOURS.COUPONS +COUPDAYSNC = NB.JOURS.COUPON.SUIV +COUPNCD = DATE.COUPON.SUIV +COUPNUM = NB.COUPONS +COUPPCD = DATE.COUPON.PREC +CUMIPMT = CUMUL.INTER +CUMPRINC = CUMUL.PRINCPER +DB = DB +DDB = DDB +DISC = TAUX.ESCOMPTE +DOLLARDE = PRIX.DEC +DOLLARFR = PRIX.FRAC +DURATION = DUREE +EFFECT = TAUX.EFFECTIF +FV = VC +FVSCHEDULE = VC.PAIEMENTS +INTRATE = TAUX.INTERET +IPMT = INTPER +IRR = TRI +ISPMT = ISPMT +MDURATION = DUREE.MODIFIEE +MIRR = TRIM +NOMINAL = TAUX.NOMINAL +NPER = NPM +NPV = VAN +ODDFPRICE = PRIX.PCOUPON.IRREG +ODDFYIELD = REND.PCOUPON.IRREG +ODDLPRICE = PRIX.DCOUPON.IRREG +ODDLYIELD = REND.DCOUPON.IRREG +PDURATION = PDUREE +PMT = VPM +PPMT = PRINCPER +PRICE = PRIX.TITRE +PRICEDISC = VALEUR.ENCAISSEMENT +PRICEMAT = PRIX.TITRE.ECHEANCE +PV = VA +RATE = TAUX +RECEIVED = VALEUR.NOMINALE +RRI = TAUX.INT.EQUIV +SLN = AMORLIN +SYD = SYD +TBILLEQ = TAUX.ESCOMPTE.R +TBILLPRICE = PRIX.BON.TRESOR +TBILLYIELD = RENDEMENT.BON.TRESOR +VDB = VDB +XIRR = TRI.PAIEMENTS +XNPV = VAN.PAIEMENTS +YIELD = RENDEMENT.TITRE +YIELDDISC = RENDEMENT.SIMPLE +YIELDMAT = RENDEMENT.TITRE.ECHEANCE + +## +## Fonctions d’information (Information Functions) +## +CELL = CELLULE +ERROR.TYPE = TYPE.ERREUR +INFO = INFORMATIONS +ISBLANK = ESTVIDE +ISERR = ESTERR +ISERROR = ESTERREUR +ISEVEN = EST.PAIR +ISFORMULA = ESTFORMULE +ISLOGICAL = ESTLOGIQUE +ISNA = ESTNA +ISNONTEXT = ESTNONTEXTE +ISNUMBER = ESTNUM +ISODD = EST.IMPAIR +ISREF = ESTREF +ISTEXT = ESTTEXTE +N = N +NA = NA +SHEET = FEUILLE +SHEETS = FEUILLES +TYPE = TYPE + +## +## Fonctions logiques (Logical Functions) +## +AND = ET +FALSE = FAUX +IF = SI +IFERROR = SIERREUR +IFNA = SI.NON.DISP +IFS = SI.CONDITIONS +NOT = NON +OR = OU +SWITCH = SI.MULTIPLE +TRUE = VRAI +XOR = OUX + +## +## Fonctions de recherche et de référence (Lookup & Reference Functions) +## +ADDRESS = ADRESSE +AREAS = ZONES +CHOOSE = CHOISIR +COLUMN = COLONNE +COLUMNS = COLONNES +FORMULATEXT = FORMULETEXTE +GETPIVOTDATA = LIREDONNEESTABCROISDYNAMIQUE +HLOOKUP = RECHERCHEH +HYPERLINK = LIEN_HYPERTEXTE +INDEX = INDEX +INDIRECT = INDIRECT +LOOKUP = RECHERCHE +MATCH = EQUIV +OFFSET = DECALER +ROW = LIGNE +ROWS = LIGNES +RTD = RTD +TRANSPOSE = TRANSPOSE +VLOOKUP = RECHERCHEV +*RC = LC + +## +## Fonctions mathématiques et trigonométriques (Math & Trig Functions) +## +ABS = ABS +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = AGREGAT +ARABIC = CHIFFRE.ARABE +ASIN = ASIN +ASINH = ASINH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = BASE +CEILING.MATH = PLAFOND.MATH +CEILING.PRECISE = PLAFOND.PRECIS +COMBIN = COMBIN +COMBINA = COMBINA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DECIMAL +DEGREES = DEGRES +ECMA.CEILING = ECMA.PLAFOND +EVEN = PAIR +EXP = EXP +FACT = FACT +FACTDOUBLE = FACTDOUBLE +FLOOR.MATH = PLANCHER.MATH +FLOOR.PRECISE = PLANCHER.PRECIS +GCD = PGCD +INT = ENT +ISO.CEILING = ISO.PLAFOND +LCM = PPCM +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = DETERMAT +MINVERSE = INVERSEMAT +MMULT = PRODUITMAT +MOD = MOD +MROUND = ARRONDI.AU.MULTIPLE +MULTINOMIAL = MULTINOMIALE +MUNIT = MATRICE.UNITAIRE +ODD = IMPAIR +PI = PI +POWER = PUISSANCE +PRODUCT = PRODUIT +QUOTIENT = QUOTIENT +RADIANS = RADIANS +RAND = ALEA +RANDBETWEEN = ALEA.ENTRE.BORNES +ROMAN = ROMAIN +ROUND = ARRONDI +ROUNDDOWN = ARRONDI.INF +ROUNDUP = ARRONDI.SUP +SEC = SEC +SECH = SECH +SERIESSUM = SOMME.SERIES +SIGN = SIGNE +SIN = SIN +SINH = SINH +SQRT = RACINE +SQRTPI = RACINE.PI +SUBTOTAL = SOUS.TOTAL +SUM = SOMME +SUMIF = SOMME.SI +SUMIFS = SOMME.SI.ENS +SUMPRODUCT = SOMMEPROD +SUMSQ = SOMME.CARRES +SUMX2MY2 = SOMME.X2MY2 +SUMX2PY2 = SOMME.X2PY2 +SUMXMY2 = SOMME.XMY2 +TAN = TAN +TANH = TANH +TRUNC = TRONQUE + +## +## Fonctions statistiques (Statistical Functions) +## +AVEDEV = ECART.MOYEN +AVERAGE = MOYENNE +AVERAGEA = AVERAGEA +AVERAGEIF = MOYENNE.SI +AVERAGEIFS = MOYENNE.SI.ENS +BETA.DIST = LOI.BETA.N +BETA.INV = BETA.INVERSE.N +BINOM.DIST = LOI.BINOMIALE.N +BINOM.DIST.RANGE = LOI.BINOMIALE.SERIE +BINOM.INV = LOI.BINOMIALE.INVERSE +CHISQ.DIST = LOI.KHIDEUX.N +CHISQ.DIST.RT = LOI.KHIDEUX.DROITE +CHISQ.INV = LOI.KHIDEUX.INVERSE +CHISQ.INV.RT = LOI.KHIDEUX.INVERSE.DROITE +CHISQ.TEST = CHISQ.TEST +CONFIDENCE.NORM = INTERVALLE.CONFIANCE.NORMAL +CONFIDENCE.T = INTERVALLE.CONFIANCE.STUDENT +CORREL = COEFFICIENT.CORRELATION +COUNT = NB +COUNTA = NBVAL +COUNTBLANK = NB.VIDE +COUNTIF = NB.SI +COUNTIFS = NB.SI.ENS +COVARIANCE.P = COVARIANCE.PEARSON +COVARIANCE.S = COVARIANCE.STANDARD +DEVSQ = SOMME.CARRES.ECARTS +EXPON.DIST = LOI.EXPONENTIELLE.N +F.DIST = LOI.F.N +F.DIST.RT = LOI.F.DROITE +F.INV = INVERSE.LOI.F.N +F.INV.RT = INVERSE.LOI.F.DROITE +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHER.INVERSE +FORECAST.ETS = PREVISION.ETS +FORECAST.ETS.CONFINT = PREVISION.ETS.CONFINT +FORECAST.ETS.SEASONALITY = PREVISION.ETS.CARACTERESAISONNIER +FORECAST.ETS.STAT = PREVISION.ETS.STAT +FORECAST.LINEAR = PREVISION.LINEAIRE +FREQUENCY = FREQUENCE +GAMMA = GAMMA +GAMMA.DIST = LOI.GAMMA.N +GAMMA.INV = LOI.GAMMA.INVERSE.N +GAMMALN = LNGAMMA +GAMMALN.PRECISE = LNGAMMA.PRECIS +GAUSS = GAUSS +GEOMEAN = MOYENNE.GEOMETRIQUE +GROWTH = CROISSANCE +HARMEAN = MOYENNE.HARMONIQUE +HYPGEOM.DIST = LOI.HYPERGEOMETRIQUE.N +INTERCEPT = ORDONNEE.ORIGINE +KURT = KURTOSIS +LARGE = GRANDE.VALEUR +LINEST = DROITEREG +LOGEST = LOGREG +LOGNORM.DIST = LOI.LOGNORMALE.N +LOGNORM.INV = LOI.LOGNORMALE.INVERSE.N +MAX = MAX +MAXA = MAXA +MAXIFS = MAX.SI +MEDIAN = MEDIANE +MIN = MIN +MINA = MINA +MINIFS = MIN.SI +MODE.MULT = MODE.MULTIPLE +MODE.SNGL = MODE.SIMPLE +NEGBINOM.DIST = LOI.BINOMIALE.NEG.N +NORM.DIST = LOI.NORMALE.N +NORM.INV = LOI.NORMALE.INVERSE.N +NORM.S.DIST = LOI.NORMALE.STANDARD.N +NORM.S.INV = LOI.NORMALE.STANDARD.INVERSE.N +PEARSON = PEARSON +PERCENTILE.EXC = CENTILE.EXCLURE +PERCENTILE.INC = CENTILE.INCLURE +PERCENTRANK.EXC = RANG.POURCENTAGE.EXCLURE +PERCENTRANK.INC = RANG.POURCENTAGE.INCLURE +PERMUT = PERMUTATION +PERMUTATIONA = PERMUTATIONA +PHI = PHI +POISSON.DIST = LOI.POISSON.N +PROB = PROBABILITE +QUARTILE.EXC = QUARTILE.EXCLURE +QUARTILE.INC = QUARTILE.INCLURE +RANK.AVG = MOYENNE.RANG +RANK.EQ = EQUATION.RANG +RSQ = COEFFICIENT.DETERMINATION +SKEW = COEFFICIENT.ASYMETRIE +SKEW.P = COEFFICIENT.ASYMETRIE.P +SLOPE = PENTE +SMALL = PETITE.VALEUR +STANDARDIZE = CENTREE.REDUITE +STDEV.P = ECARTYPE.PEARSON +STDEV.S = ECARTYPE.STANDARD +STDEVA = STDEVA +STDEVPA = STDEVPA +STEYX = ERREUR.TYPE.XY +T.DIST = LOI.STUDENT.N +T.DIST.2T = LOI.STUDENT.BILATERALE +T.DIST.RT = LOI.STUDENT.DROITE +T.INV = LOI.STUDENT.INVERSE.N +T.INV.2T = LOI.STUDENT.INVERSE.BILATERALE +T.TEST = T.TEST +TREND = TENDANCE +TRIMMEAN = MOYENNE.REDUITE +VAR.P = VAR.P.N +VAR.S = VAR.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = LOI.WEIBULL.N +Z.TEST = Z.TEST + +## +## Fonctions de texte (Text Functions) +## +BAHTTEXT = BAHTTEXT +CHAR = CAR +CLEAN = EPURAGE +CODE = CODE +CONCAT = CONCAT +DOLLAR = DEVISE +EXACT = EXACT +FIND = TROUVE +FIXED = CTXT +LEFT = GAUCHE +LEN = NBCAR +LOWER = MINUSCULE +MID = STXT +NUMBERVALUE = VALEURNOMBRE +PHONETIC = PHONETIQUE +PROPER = NOMPROPRE +REPLACE = REMPLACER +REPT = REPT +RIGHT = DROITE +SEARCH = CHERCHE +SUBSTITUTE = SUBSTITUE +T = T +TEXT = TEXTE +TEXTJOIN = JOINDRE.TEXTE +TRIM = SUPPRESPACE +UNICHAR = UNICAR +UNICODE = UNICODE +UPPER = MAJUSCULE +VALUE = CNUM + +## +## Fonctions web (Web Functions) +## +ENCODEURL = URLENCODAGE +FILTERXML = FILTRE.XML +WEBSERVICE = SERVICEWEB + +## +## Fonctions de compatibilité (Compatibility Functions) +## +BETADIST = LOI.BETA +BETAINV = BETA.INVERSE +BINOMDIST = LOI.BINOMIALE +CEILING = PLAFOND +CHIDIST = LOI.KHIDEUX +CHIINV = KHIDEUX.INVERSE +CHITEST = TEST.KHIDEUX +CONCATENATE = CONCATENER +CONFIDENCE = INTERVALLE.CONFIANCE +COVAR = COVARIANCE +CRITBINOM = CRITERE.LOI.BINOMIALE +EXPONDIST = LOI.EXPONENTIELLE +FDIST = LOI.F +FINV = INVERSE.LOI.F +FLOOR = PLANCHER +FORECAST = PREVISION +FTEST = TEST.F +GAMMADIST = LOI.GAMMA +GAMMAINV = LOI.GAMMA.INVERSE +HYPGEOMDIST = LOI.HYPERGEOMETRIQUE +LOGINV = LOI.LOGNORMALE.INVERSE +LOGNORMDIST = LOI.LOGNORMALE +MODE = MODE +NEGBINOMDIST = LOI.BINOMIALE.NEG +NORMDIST = LOI.NORMALE +NORMINV = LOI.NORMALE.INVERSE +NORMSDIST = LOI.NORMALE.STANDARD +NORMSINV = LOI.NORMALE.STANDARD.INVERSE +PERCENTILE = CENTILE +PERCENTRANK = RANG.POURCENTAGE +POISSON = LOI.POISSON +QUARTILE = QUARTILE +RANK = RANG +STDEV = ECARTYPE +STDEVP = ECARTYPEP +TDIST = LOI.STUDENT +TINV = LOI.STUDENT.INVERSE +TTEST = TEST.STUDENT +VAR = VAR +VARP = VAR.P +WEIBULL = LOI.WEIBULL +ZTEST = TEST.Z diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/hu/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/hu/config new file mode 100644 index 0000000..dc585d7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/hu/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Magyar (Hungarian) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #NULLA! +DIV0 = #ZÉRÓOSZTÓ! +VALUE = #ÉRTÉK! +REF = #HIV! +NAME = #NÉV? +NUM = #SZÁM! +NA = #HIÁNYZIK diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/hu/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/hu/functions new file mode 100644 index 0000000..4a375ea --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/hu/functions @@ -0,0 +1,538 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Magyar (Hungarian) +## +############################################################ + + +## +## Kockafüggvények (Cube Functions) +## +CUBEKPIMEMBER = KOCKA.FŐTELJMUT +CUBEMEMBER = KOCKA.TAG +CUBEMEMBERPROPERTY = KOCKA.TAG.TUL +CUBERANKEDMEMBER = KOCKA.HALM.ELEM +CUBESET = KOCKA.HALM +CUBESETCOUNT = KOCKA.HALM.DB +CUBEVALUE = KOCKA.ÉRTÉK + +## +## Adatbázis-kezelő függvények (Database Functions) +## +DAVERAGE = AB.ÁTLAG +DCOUNT = AB.DARAB +DCOUNTA = AB.DARAB2 +DGET = AB.MEZŐ +DMAX = AB.MAX +DMIN = AB.MIN +DPRODUCT = AB.SZORZAT +DSTDEV = AB.SZÓRÁS +DSTDEVP = AB.SZÓRÁS2 +DSUM = AB.SZUM +DVAR = AB.VAR +DVARP = AB.VAR2 + +## +## Dátumfüggvények (Date & Time Functions) +## +DATE = DÁTUM +DATEDIF = DÁTUMTÓLIG +DATESTRING = DÁTUMSZÖVEG +DATEVALUE = DÁTUMÉRTÉK +DAY = NAP +DAYS = NAPOK +DAYS360 = NAP360 +EDATE = KALK.DÁTUM +EOMONTH = HÓNAP.UTOLSÓ.NAP +HOUR = ÓRA +ISOWEEKNUM = ISO.HÉT.SZÁMA +MINUTE = PERCEK +MONTH = HÓNAP +NETWORKDAYS = ÖSSZ.MUNKANAP +NETWORKDAYS.INTL = ÖSSZ.MUNKANAP.INTL +NOW = MOST +SECOND = MPERC +THAIDAYOFWEEK = THAIHÉTNAPJA +THAIMONTHOFYEAR = THAIHÓNAP +THAIYEAR = THAIÉV +TIME = IDŐ +TIMEVALUE = IDŐÉRTÉK +TODAY = MA +WEEKDAY = HÉT.NAPJA +WEEKNUM = HÉT.SZÁMA +WORKDAY = KALK.MUNKANAP +WORKDAY.INTL = KALK.MUNKANAP.INTL +YEAR = ÉV +YEARFRAC = TÖRTÉV + +## +## Mérnöki függvények (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BIN.DEC +BIN2HEX = BIN.HEX +BIN2OCT = BIN.OKT +BITAND = BIT.ÉS +BITLSHIFT = BIT.BAL.ELTOL +BITOR = BIT.VAGY +BITRSHIFT = BIT.JOBB.ELTOL +BITXOR = BIT.XVAGY +COMPLEX = KOMPLEX +CONVERT = KONVERTÁLÁS +DEC2BIN = DEC.BIN +DEC2HEX = DEC.HEX +DEC2OCT = DEC.OKT +DELTA = DELTA +ERF = HIBAF +ERF.PRECISE = HIBAF.PONTOS +ERFC = HIBAF.KOMPLEMENTER +ERFC.PRECISE = HIBAFKOMPLEMENTER.PONTOS +GESTEP = KÜSZÖBNÉL.NAGYOBB +HEX2BIN = HEX.BIN +HEX2DEC = HEX.DEC +HEX2OCT = HEX.OKT +IMABS = KÉPZ.ABSZ +IMAGINARY = KÉPZETES +IMARGUMENT = KÉPZ.ARGUMENT +IMCONJUGATE = KÉPZ.KONJUGÁLT +IMCOS = KÉPZ.COS +IMCOSH = KÉPZ.COSH +IMCOT = KÉPZ.COT +IMCSC = KÉPZ.CSC +IMCSCH = KÉPZ.CSCH +IMDIV = KÉPZ.HÁNYAD +IMEXP = KÉPZ.EXP +IMLN = KÉPZ.LN +IMLOG10 = KÉPZ.LOG10 +IMLOG2 = KÉPZ.LOG2 +IMPOWER = KÉPZ.HATV +IMPRODUCT = KÉPZ.SZORZAT +IMREAL = KÉPZ.VALÓS +IMSEC = KÉPZ.SEC +IMSECH = KÉPZ.SECH +IMSIN = KÉPZ.SIN +IMSINH = KÉPZ.SINH +IMSQRT = KÉPZ.GYÖK +IMSUB = KÉPZ.KÜL +IMSUM = KÉPZ.ÖSSZEG +IMTAN = KÉPZ.TAN +OCT2BIN = OKT.BIN +OCT2DEC = OKT.DEC +OCT2HEX = OKT.HEX + +## +## Pénzügyi függvények (Financial Functions) +## +ACCRINT = IDŐSZAKI.KAMAT +ACCRINTM = LEJÁRATI.KAMAT +AMORDEGRC = ÉRTÉKCSÖKK.TÉNYEZŐVEL +AMORLINC = ÉRTÉKCSÖKK +COUPDAYBS = SZELVÉNYIDŐ.KEZDETTŐL +COUPDAYS = SZELVÉNYIDŐ +COUPDAYSNC = SZELVÉNYIDŐ.KIFIZETÉSTŐL +COUPNCD = ELSŐ.SZELVÉNYDÁTUM +COUPNUM = SZELVÉNYSZÁM +COUPPCD = UTOLSÓ.SZELVÉNYDÁTUM +CUMIPMT = ÖSSZES.KAMAT +CUMPRINC = ÖSSZES.TŐKERÉSZ +DB = KCS2 +DDB = KCSA +DISC = LESZÁM +DOLLARDE = FORINT.DEC +DOLLARFR = FORINT.TÖRT +DURATION = KAMATÉRZ +EFFECT = TÉNYLEGES +FV = JBÉ +FVSCHEDULE = KJÉ +INTRATE = KAMATRÁTA +IPMT = RRÉSZLET +IRR = BMR +ISPMT = LRÉSZLETKAMAT +MDURATION = MKAMATÉRZ +MIRR = MEGTÉRÜLÉS +NOMINAL = NÉVLEGES +NPER = PER.SZÁM +NPV = NMÉ +ODDFPRICE = ELTÉRŐ.EÁR +ODDFYIELD = ELTÉRŐ.EHOZAM +ODDLPRICE = ELTÉRŐ.UÁR +ODDLYIELD = ELTÉRŐ.UHOZAM +PDURATION = KAMATÉRZ.PER +PMT = RÉSZLET +PPMT = PRÉSZLET +PRICE = ÁR +PRICEDISC = ÁR.LESZÁM +PRICEMAT = ÁR.LEJÁRAT +PV = MÉ +RATE = RÁTA +RECEIVED = KAPOTT +RRI = MR +SLN = LCSA +SYD = ÉSZÖ +TBILLEQ = KJEGY.EGYENÉRT +TBILLPRICE = KJEGY.ÁR +TBILLYIELD = KJEGY.HOZAM +VDB = ÉCSRI +XIRR = XBMR +XNPV = XNJÉ +YIELD = HOZAM +YIELDDISC = HOZAM.LESZÁM +YIELDMAT = HOZAM.LEJÁRAT + +## +## Információs függvények (Information Functions) +## +CELL = CELLA +ERROR.TYPE = HIBA.TÍPUS +INFO = INFÓ +ISBLANK = ÜRES +ISERR = HIBA.E +ISERROR = HIBÁS +ISEVEN = PÁROSE +ISFORMULA = KÉPLET +ISLOGICAL = LOGIKAI +ISNA = NINCS +ISNONTEXT = NEM.SZÖVEG +ISNUMBER = SZÁM +ISODD = PÁRATLANE +ISREF = HIVATKOZÁS +ISTEXT = SZÖVEG.E +N = S +NA = HIÁNYZIK +SHEET = LAP +SHEETS = LAPOK +TYPE = TÍPUS + +## +## Logikai függvények (Logical Functions) +## +AND = ÉS +FALSE = HAMIS +IF = HA +IFERROR = HAHIBA +IFNA = HAHIÁNYZIK +IFS = HAELSŐIGAZ +NOT = NEM +OR = VAGY +SWITCH = ÁTVÁLT +TRUE = IGAZ +XOR = XVAGY + +## +## Keresési és hivatkozási függvények (Lookup & Reference Functions) +## +ADDRESS = CÍM +AREAS = TERÜLET +CHOOSE = VÁLASZT +COLUMN = OSZLOP +COLUMNS = OSZLOPOK +FORMULATEXT = KÉPLETSZÖVEG +GETPIVOTDATA = KIMUTATÁSADATOT.VESZ +HLOOKUP = VKERES +HYPERLINK = HIPERHIVATKOZÁS +INDEX = INDEX +INDIRECT = INDIREKT +LOOKUP = KERES +MATCH = HOL.VAN +OFFSET = ELTOLÁS +ROW = SOR +ROWS = SOROK +RTD = VIA +TRANSPOSE = TRANSZPONÁLÁS +VLOOKUP = FKERES +*RC = SO + +## +## Matematikai és trigonometrikus függvények (Math & Trig Functions) +## +ABS = ABS +ACOS = ARCCOS +ACOSH = ACOSH +ACOT = ARCCOT +ACOTH = ARCCOTH +AGGREGATE = ÖSSZESÍT +ARABIC = ARAB +ASIN = ARCSIN +ASINH = ASINH +ATAN = ARCTAN +ATAN2 = ARCTAN2 +ATANH = ATANH +BASE = ALAP +CEILING.MATH = PLAFON.MAT +CEILING.PRECISE = PLAFON.PONTOS +COMBIN = KOMBINÁCIÓK +COMBINA = KOMBINÁCIÓK.ISM +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = TIZEDES +DEGREES = FOK +ECMA.CEILING = ECMA.PLAFON +EVEN = PÁROS +EXP = KITEVŐ +FACT = FAKT +FACTDOUBLE = FAKTDUPLA +FLOOR.MATH = PADLÓ.MAT +FLOOR.PRECISE = PADLÓ.PONTOS +GCD = LKO +INT = INT +ISO.CEILING = ISO.PLAFON +LCM = LKT +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MDETERM +MINVERSE = INVERZ.MÁTRIX +MMULT = MSZORZAT +MOD = MARADÉK +MROUND = TÖBBSZ.KEREKÍT +MULTINOMIAL = SZORHÁNYFAKT +MUNIT = MMÁTRIX +ODD = PÁRATLAN +PI = PI +POWER = HATVÁNY +PRODUCT = SZORZAT +QUOTIENT = KVÓCIENS +RADIANS = RADIÁN +RAND = VÉL +RANDBETWEEN = VÉLETLEN.KÖZÖTT +ROMAN = RÓMAI +ROUND = KEREKÍTÉS +ROUNDBAHTDOWN = BAHTKEREK.LE +ROUNDBAHTUP = BAHTKEREK.FEL +ROUNDDOWN = KEREK.LE +ROUNDUP = KEREK.FEL +SEC = SEC +SECH = SECH +SERIESSUM = SORÖSSZEG +SIGN = ELŐJEL +SIN = SIN +SINH = SINH +SQRT = GYÖK +SQRTPI = GYÖKPI +SUBTOTAL = RÉSZÖSSZEG +SUM = SZUM +SUMIF = SZUMHA +SUMIFS = SZUMHATÖBB +SUMPRODUCT = SZORZATÖSSZEG +SUMSQ = NÉGYZETÖSSZEG +SUMX2MY2 = SZUMX2BŐLY2 +SUMX2PY2 = SZUMX2MEGY2 +SUMXMY2 = SZUMXBŐLY2 +TAN = TAN +TANH = TANH +TRUNC = CSONK + +## +## Statisztikai függvények (Statistical Functions) +## +AVEDEV = ÁTL.ELTÉRÉS +AVERAGE = ÁTLAG +AVERAGEA = ÁTLAGA +AVERAGEIF = ÁTLAGHA +AVERAGEIFS = ÁTLAGHATÖBB +BETA.DIST = BÉTA.ELOSZL +BETA.INV = BÉTA.INVERZ +BINOM.DIST = BINOM.ELOSZL +BINOM.DIST.RANGE = BINOM.ELOSZL.TART +BINOM.INV = BINOM.INVERZ +CHISQ.DIST = KHINÉGYZET.ELOSZLÁS +CHISQ.DIST.RT = KHINÉGYZET.ELOSZLÁS.JOBB +CHISQ.INV = KHINÉGYZET.INVERZ +CHISQ.INV.RT = KHINÉGYZET.INVERZ.JOBB +CHISQ.TEST = KHINÉGYZET.PRÓBA +CONFIDENCE.NORM = MEGBÍZHATÓSÁG.NORM +CONFIDENCE.T = MEGBÍZHATÓSÁG.T +CORREL = KORREL +COUNT = DARAB +COUNTA = DARAB2 +COUNTBLANK = DARABÜRES +COUNTIF = DARABTELI +COUNTIFS = DARABHATÖBB +COVARIANCE.P = KOVARIANCIA.S +COVARIANCE.S = KOVARIANCIA.M +DEVSQ = SQ +EXPON.DIST = EXP.ELOSZL +F.DIST = F.ELOSZL +F.DIST.RT = F.ELOSZLÁS.JOBB +F.INV = F.INVERZ +F.INV.RT = F.INVERZ.JOBB +F.TEST = F.PRÓB +FISHER = FISHER +FISHERINV = INVERZ.FISHER +FORECAST.ETS = ELŐREJELZÉS.ESIM +FORECAST.ETS.CONFINT = ELŐREJELZÉS.ESIM.KONFINT +FORECAST.ETS.SEASONALITY = ELŐREJELZÉS.ESIM.SZEZONALITÁS +FORECAST.ETS.STAT = ELŐREJELZÉS.ESIM.STAT +FORECAST.LINEAR = ELŐREJELZÉS.LINEÁRIS +FREQUENCY = GYAKORISÁG +GAMMA = GAMMA +GAMMA.DIST = GAMMA.ELOSZL +GAMMA.INV = GAMMA.INVERZ +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.PONTOS +GAUSS = GAUSS +GEOMEAN = MÉRTANI.KÖZÉP +GROWTH = NÖV +HARMEAN = HARM.KÖZÉP +HYPGEOM.DIST = HIPGEOM.ELOSZLÁS +INTERCEPT = METSZ +KURT = CSÚCSOSSÁG +LARGE = NAGY +LINEST = LIN.ILL +LOGEST = LOG.ILL +LOGNORM.DIST = LOGNORM.ELOSZLÁS +LOGNORM.INV = LOGNORM.INVERZ +MAX = MAX +MAXA = MAXA +MAXIFS = MAXHA +MEDIAN = MEDIÁN +MIN = MIN +MINA = MIN2 +MINIFS = MINHA +MODE.MULT = MÓDUSZ.TÖBB +MODE.SNGL = MÓDUSZ.EGY +NEGBINOM.DIST = NEGBINOM.ELOSZLÁS +NORM.DIST = NORM.ELOSZLÁS +NORM.INV = NORM.INVERZ +NORM.S.DIST = NORM.S.ELOSZLÁS +NORM.S.INV = NORM.S.INVERZ +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTILIS.KIZÁR +PERCENTILE.INC = PERCENTILIS.TARTALMAZ +PERCENTRANK.EXC = SZÁZALÉKRANG.KIZÁR +PERCENTRANK.INC = SZÁZALÉKRANG.TARTALMAZ +PERMUT = VARIÁCIÓK +PERMUTATIONA = VARIÁCIÓK.ISM +PHI = FI +POISSON.DIST = POISSON.ELOSZLÁS +PROB = VALÓSZÍNŰSÉG +QUARTILE.EXC = KVARTILIS.KIZÁR +QUARTILE.INC = KVARTILIS.TARTALMAZ +RANK.AVG = RANG.ÁTL +RANK.EQ = RANG.EGY +RSQ = RNÉGYZET +SKEW = FERDESÉG +SKEW.P = FERDESÉG.P +SLOPE = MEREDEKSÉG +SMALL = KICSI +STANDARDIZE = NORMALIZÁLÁS +STDEV.P = SZÓR.S +STDEV.S = SZÓR.M +STDEVA = SZÓRÁSA +STDEVPA = SZÓRÁSPA +STEYX = STHIBAYX +T.DIST = T.ELOSZL +T.DIST.2T = T.ELOSZLÁS.2SZ +T.DIST.RT = T.ELOSZLÁS.JOBB +T.INV = T.INVERZ +T.INV.2T = T.INVERZ.2SZ +T.TEST = T.PRÓB +TREND = TREND +TRIMMEAN = RÉSZÁTLAG +VAR.P = VAR.S +VAR.S = VAR.M +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = WEIBULL.ELOSZLÁS +Z.TEST = Z.PRÓB + +## +## Szövegműveletekhez használható függvények (Text Functions) +## +BAHTTEXT = BAHTSZÖVEG +CHAR = KARAKTER +CLEAN = TISZTÍT +CODE = KÓD +CONCAT = FŰZ +DOLLAR = FORINT +EXACT = AZONOS +FIND = SZÖVEG.TALÁL +FIXED = FIX +ISTHAIDIGIT = ON.THAI.NUMERO +LEFT = BAL +LEN = HOSSZ +LOWER = KISBETŰ +MID = KÖZÉP +NUMBERSTRING = SZÁM.BETŰVEL +NUMBERVALUE = SZÁMÉRTÉK +PHONETIC = FONETIKUS +PROPER = TNÉV +REPLACE = CSERE +REPT = SOKSZOR +RIGHT = JOBB +SEARCH = SZÖVEG.KERES +SUBSTITUTE = HELYETTE +T = T +TEXT = SZÖVEG +TEXTJOIN = SZÖVEGÖSSZEFŰZÉS +THAIDIGIT = THAISZÁM +THAINUMSOUND = THAISZÁMHANG +THAINUMSTRING = THAISZÁMKAR +THAISTRINGLENGTH = THAIKARHOSSZ +TRIM = KIMETSZ +UNICHAR = UNIKARAKTER +UNICODE = UNICODE +UPPER = NAGYBETŰS +VALUE = ÉRTÉK + +## +## Webes függvények (Web Functions) +## +ENCODEURL = URL.KÓDOL +FILTERXML = XMLSZŰRÉS +WEBSERVICE = WEBSZOLGÁLTATÁS + +## +## Kompatibilitási függvények (Compatibility Functions) +## +BETADIST = BÉTA.ELOSZLÁS +BETAINV = INVERZ.BÉTA +BINOMDIST = BINOM.ELOSZLÁS +CEILING = PLAFON +CHIDIST = KHI.ELOSZLÁS +CHIINV = INVERZ.KHI +CHITEST = KHI.PRÓBA +CONCATENATE = ÖSSZEFŰZ +CONFIDENCE = MEGBÍZHATÓSÁG +COVAR = KOVAR +CRITBINOM = KRITBINOM +EXPONDIST = EXP.ELOSZLÁS +FDIST = F.ELOSZLÁS +FINV = INVERZ.F +FLOOR = PADLÓ +FORECAST = ELŐREJELZÉS +FTEST = F.PRÓBA +GAMMADIST = GAMMA.ELOSZLÁS +GAMMAINV = INVERZ.GAMMA +HYPGEOMDIST = HIPERGEOM.ELOSZLÁS +LOGINV = INVERZ.LOG.ELOSZLÁS +LOGNORMDIST = LOG.ELOSZLÁS +MODE = MÓDUSZ +NEGBINOMDIST = NEGBINOM.ELOSZL +NORMDIST = NORM.ELOSZL +NORMINV = INVERZ.NORM +NORMSDIST = STNORMELOSZL +NORMSINV = INVERZ.STNORM +PERCENTILE = PERCENTILIS +PERCENTRANK = SZÁZALÉKRANG +POISSON = POISSON +QUARTILE = KVARTILIS +RANK = SORSZÁM +STDEV = SZÓRÁS +STDEVP = SZÓRÁSP +TDIST = T.ELOSZLÁS +TINV = INVERZ.T +TTEST = T.PRÓBA +VAR = VAR +VARP = VARP +WEIBULL = WEIBULL +ZTEST = Z.PRÓBA diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/it/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/it/config new file mode 100644 index 0000000..5c1e495 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/it/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Italiano (Italian) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL +DIV0 +VALUE = #VALORE! +REF = #RIF! +NAME = #NOME? +NUM +NA = #N/D diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/it/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/it/functions new file mode 100644 index 0000000..c14ed85 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/it/functions @@ -0,0 +1,537 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Italiano (Italian) +## +############################################################ + + +## +## Funzioni cubo (Cube Functions) +## +CUBEKPIMEMBER = MEMBRO.KPI.CUBO +CUBEMEMBER = MEMBRO.CUBO +CUBEMEMBERPROPERTY = PROPRIETÀ.MEMBRO.CUBO +CUBERANKEDMEMBER = MEMBRO.CUBO.CON.RANGO +CUBESET = SET.CUBO +CUBESETCOUNT = CONTA.SET.CUBO +CUBEVALUE = VALORE.CUBO + +## +## Funzioni di database (Database Functions) +## +DAVERAGE = DB.MEDIA +DCOUNT = DB.CONTA.NUMERI +DCOUNTA = DB.CONTA.VALORI +DGET = DB.VALORI +DMAX = DB.MAX +DMIN = DB.MIN +DPRODUCT = DB.PRODOTTO +DSTDEV = DB.DEV.ST +DSTDEVP = DB.DEV.ST.POP +DSUM = DB.SOMMA +DVAR = DB.VAR +DVARP = DB.VAR.POP + +## +## Funzioni data e ora (Date & Time Functions) +## +DATE = DATA +DATEDIF = DATA.DIFF +DATESTRING = DATA.STRINGA +DATEVALUE = DATA.VALORE +DAY = GIORNO +DAYS = GIORNI +DAYS360 = GIORNO360 +EDATE = DATA.MESE +EOMONTH = FINE.MESE +HOUR = ORA +ISOWEEKNUM = NUM.SETTIMANA.ISO +MINUTE = MINUTO +MONTH = MESE +NETWORKDAYS = GIORNI.LAVORATIVI.TOT +NETWORKDAYS.INTL = GIORNI.LAVORATIVI.TOT.INTL +NOW = ADESSO +SECOND = SECONDO +THAIDAYOFWEEK = THAIGIORNODELLASETTIMANA +THAIMONTHOFYEAR = THAIMESEDELLANNO +THAIYEAR = THAIANNO +TIME = ORARIO +TIMEVALUE = ORARIO.VALORE +TODAY = OGGI +WEEKDAY = GIORNO.SETTIMANA +WEEKNUM = NUM.SETTIMANA +WORKDAY = GIORNO.LAVORATIVO +WORKDAY.INTL = GIORNO.LAVORATIVO.INTL +YEAR = ANNO +YEARFRAC = FRAZIONE.ANNO + +## +## Funzioni ingegneristiche (Engineering Functions) +## +BESSELI = BESSEL.I +BESSELJ = BESSEL.J +BESSELK = BESSEL.K +BESSELY = BESSEL.Y +BIN2DEC = BINARIO.DECIMALE +BIN2HEX = BINARIO.HEX +BIN2OCT = BINARIO.OCT +BITAND = BITAND +BITLSHIFT = BIT.SPOSTA.SX +BITOR = BITOR +BITRSHIFT = BIT.SPOSTA.DX +BITXOR = BITXOR +COMPLEX = COMPLESSO +CONVERT = CONVERTI +DEC2BIN = DECIMALE.BINARIO +DEC2HEX = DECIMALE.HEX +DEC2OCT = DECIMALE.OCT +DELTA = DELTA +ERF = FUNZ.ERRORE +ERF.PRECISE = FUNZ.ERRORE.PRECISA +ERFC = FUNZ.ERRORE.COMP +ERFC.PRECISE = FUNZ.ERRORE.COMP.PRECISA +GESTEP = SOGLIA +HEX2BIN = HEX.BINARIO +HEX2DEC = HEX.DECIMALE +HEX2OCT = HEX.OCT +IMABS = COMP.MODULO +IMAGINARY = COMP.IMMAGINARIO +IMARGUMENT = COMP.ARGOMENTO +IMCONJUGATE = COMP.CONIUGATO +IMCOS = COMP.COS +IMCOSH = COMP.COSH +IMCOT = COMP.COT +IMCSC = COMP.CSC +IMCSCH = COMP.CSCH +IMDIV = COMP.DIV +IMEXP = COMP.EXP +IMLN = COMP.LN +IMLOG10 = COMP.LOG10 +IMLOG2 = COMP.LOG2 +IMPOWER = COMP.POTENZA +IMPRODUCT = COMP.PRODOTTO +IMREAL = COMP.PARTE.REALE +IMSEC = COMP.SEC +IMSECH = COMP.SECH +IMSIN = COMP.SEN +IMSINH = COMP.SENH +IMSQRT = COMP.RADQ +IMSUB = COMP.DIFF +IMSUM = COMP.SOMMA +IMTAN = COMP.TAN +OCT2BIN = OCT.BINARIO +OCT2DEC = OCT.DECIMALE +OCT2HEX = OCT.HEX + +## +## Funzioni finanziarie (Financial Functions) +## +ACCRINT = INT.MATURATO.PER +ACCRINTM = INT.MATURATO.SCAD +AMORDEGRC = AMMORT.DEGR +AMORLINC = AMMORT.PER +COUPDAYBS = GIORNI.CED.INIZ.LIQ +COUPDAYS = GIORNI.CED +COUPDAYSNC = GIORNI.CED.NUOVA +COUPNCD = DATA.CED.SUCC +COUPNUM = NUM.CED +COUPPCD = DATA.CED.PREC +CUMIPMT = INT.CUMUL +CUMPRINC = CAP.CUM +DB = AMMORT.FISSO +DDB = AMMORT +DISC = TASSO.SCONTO +DOLLARDE = VALUTA.DEC +DOLLARFR = VALUTA.FRAZ +DURATION = DURATA +EFFECT = EFFETTIVO +FV = VAL.FUT +FVSCHEDULE = VAL.FUT.CAPITALE +INTRATE = TASSO.INT +IPMT = INTERESSI +IRR = TIR.COST +ISPMT = INTERESSE.RATA +MDURATION = DURATA.M +MIRR = TIR.VAR +NOMINAL = NOMINALE +NPER = NUM.RATE +NPV = VAN +ODDFPRICE = PREZZO.PRIMO.IRR +ODDFYIELD = REND.PRIMO.IRR +ODDLPRICE = PREZZO.ULTIMO.IRR +ODDLYIELD = REND.ULTIMO.IRR +PDURATION = DURATA.P +PMT = RATA +PPMT = P.RATA +PRICE = PREZZO +PRICEDISC = PREZZO.SCONT +PRICEMAT = PREZZO.SCAD +PV = VA +RATE = TASSO +RECEIVED = RICEV.SCAD +RRI = RIT.INVEST.EFFETT +SLN = AMMORT.COST +SYD = AMMORT.ANNUO +TBILLEQ = BOT.EQUIV +TBILLPRICE = BOT.PREZZO +TBILLYIELD = BOT.REND +VDB = AMMORT.VAR +XIRR = TIR.X +XNPV = VAN.X +YIELD = REND +YIELDDISC = REND.TITOLI.SCONT +YIELDMAT = REND.SCAD + +## +## Funzioni relative alle informazioni (Information Functions) +## +CELL = CELLA +ERROR.TYPE = ERRORE.TIPO +INFO = AMBIENTE.INFO +ISBLANK = VAL.VUOTO +ISERR = VAL.ERR +ISERROR = VAL.ERRORE +ISEVEN = VAL.PARI +ISFORMULA = VAL.FORMULA +ISLOGICAL = VAL.LOGICO +ISNA = VAL.NON.DISP +ISNONTEXT = VAL.NON.TESTO +ISNUMBER = VAL.NUMERO +ISODD = VAL.DISPARI +ISREF = VAL.RIF +ISTEXT = VAL.TESTO +N = NUM +NA = NON.DISP +SHEET = FOGLIO +SHEETS = FOGLI +TYPE = TIPO + +## +## Funzioni logiche (Logical Functions) +## +AND = E +FALSE = FALSO +IF = SE +IFERROR = SE.ERRORE +IFNA = SE.NON.DISP. +IFS = PIÙ.SE +NOT = NON +OR = O +SWITCH = SWITCH +TRUE = VERO +XOR = XOR + +## +## Funzioni di ricerca e di riferimento (Lookup & Reference Functions) +## +ADDRESS = INDIRIZZO +AREAS = AREE +CHOOSE = SCEGLI +COLUMN = RIF.COLONNA +COLUMNS = COLONNE +FORMULATEXT = TESTO.FORMULA +GETPIVOTDATA = INFO.DATI.TAB.PIVOT +HLOOKUP = CERCA.ORIZZ +HYPERLINK = COLLEG.IPERTESTUALE +INDEX = INDICE +INDIRECT = INDIRETTO +LOOKUP = CERCA +MATCH = CONFRONTA +OFFSET = SCARTO +ROW = RIF.RIGA +ROWS = RIGHE +RTD = DATITEMPOREALE +TRANSPOSE = MATR.TRASPOSTA +VLOOKUP = CERCA.VERT + +## +## Funzioni matematiche e trigonometriche (Math & Trig Functions) +## +ABS = ASS +ACOS = ARCCOS +ACOSH = ARCCOSH +ACOT = ARCCOT +ACOTH = ARCCOTH +AGGREGATE = AGGREGA +ARABIC = ARABO +ASIN = ARCSEN +ASINH = ARCSENH +ATAN = ARCTAN +ATAN2 = ARCTAN.2 +ATANH = ARCTANH +BASE = BASE +CEILING.MATH = ARROTONDA.ECCESSO.MAT +CEILING.PRECISE = ARROTONDA.ECCESSO.PRECISA +COMBIN = COMBINAZIONE +COMBINA = COMBINAZIONE.VALORI +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DECIMALE +DEGREES = GRADI +ECMA.CEILING = ECMA.ARROTONDA.ECCESSO +EVEN = PARI +EXP = EXP +FACT = FATTORIALE +FACTDOUBLE = FATT.DOPPIO +FLOOR.MATH = ARROTONDA.DIFETTO.MAT +FLOOR.PRECISE = ARROTONDA.DIFETTO.PRECISA +GCD = MCD +INT = INT +ISO.CEILING = ISO.ARROTONDA.ECCESSO +LCM = MCM +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MATR.DETERM +MINVERSE = MATR.INVERSA +MMULT = MATR.PRODOTTO +MOD = RESTO +MROUND = ARROTONDA.MULTIPLO +MULTINOMIAL = MULTINOMIALE +MUNIT = MATR.UNIT +ODD = DISPARI +PI = PI.GRECO +POWER = POTENZA +PRODUCT = PRODOTTO +QUOTIENT = QUOZIENTE +RADIANS = RADIANTI +RAND = CASUALE +RANDBETWEEN = CASUALE.TRA +ROMAN = ROMANO +ROUND = ARROTONDA +ROUNDBAHTDOWN = ARROTBAHTGIU +ROUNDBAHTUP = ARROTBAHTSU +ROUNDDOWN = ARROTONDA.PER.DIF +ROUNDUP = ARROTONDA.PER.ECC +SEC = SEC +SECH = SECH +SERIESSUM = SOMMA.SERIE +SIGN = SEGNO +SIN = SEN +SINH = SENH +SQRT = RADQ +SQRTPI = RADQ.PI.GRECO +SUBTOTAL = SUBTOTALE +SUM = SOMMA +SUMIF = SOMMA.SE +SUMIFS = SOMMA.PIÙ.SE +SUMPRODUCT = MATR.SOMMA.PRODOTTO +SUMSQ = SOMMA.Q +SUMX2MY2 = SOMMA.DIFF.Q +SUMX2PY2 = SOMMA.SOMMA.Q +SUMXMY2 = SOMMA.Q.DIFF +TAN = TAN +TANH = TANH +TRUNC = TRONCA + +## +## Funzioni statistiche (Statistical Functions) +## +AVEDEV = MEDIA.DEV +AVERAGE = MEDIA +AVERAGEA = MEDIA.VALORI +AVERAGEIF = MEDIA.SE +AVERAGEIFS = MEDIA.PIÙ.SE +BETA.DIST = DISTRIB.BETA.N +BETA.INV = INV.BETA.N +BINOM.DIST = DISTRIB.BINOM.N +BINOM.DIST.RANGE = INTERVALLO.DISTRIB.BINOM.N. +BINOM.INV = INV.BINOM +CHISQ.DIST = DISTRIB.CHI.QUAD +CHISQ.DIST.RT = DISTRIB.CHI.QUAD.DS +CHISQ.INV = INV.CHI.QUAD +CHISQ.INV.RT = INV.CHI.QUAD.DS +CHISQ.TEST = TEST.CHI.QUAD +CONFIDENCE.NORM = CONFIDENZA.NORM +CONFIDENCE.T = CONFIDENZA.T +CORREL = CORRELAZIONE +COUNT = CONTA.NUMERI +COUNTA = CONTA.VALORI +COUNTBLANK = CONTA.VUOTE +COUNTIF = CONTA.SE +COUNTIFS = CONTA.PIÙ.SE +COVARIANCE.P = COVARIANZA.P +COVARIANCE.S = COVARIANZA.C +DEVSQ = DEV.Q +EXPON.DIST = DISTRIB.EXP.N +F.DIST = DISTRIBF +F.DIST.RT = DISTRIB.F.DS +F.INV = INVF +F.INV.RT = INV.F.DS +F.TEST = TESTF +FISHER = FISHER +FISHERINV = INV.FISHER +FORECAST.ETS = PREVISIONE.ETS +FORECAST.ETS.CONFINT = PREVISIONE.ETS.INTCONF +FORECAST.ETS.SEASONALITY = PREVISIONE.ETS.STAGIONALITÀ +FORECAST.ETS.STAT = PREVISIONE.ETS.STAT +FORECAST.LINEAR = PREVISIONE.LINEARE +FREQUENCY = FREQUENZA +GAMMA = GAMMA +GAMMA.DIST = DISTRIB.GAMMA.N +GAMMA.INV = INV.GAMMA.N +GAMMALN = LN.GAMMA +GAMMALN.PRECISE = LN.GAMMA.PRECISA +GAUSS = GAUSS +GEOMEAN = MEDIA.GEOMETRICA +GROWTH = CRESCITA +HARMEAN = MEDIA.ARMONICA +HYPGEOM.DIST = DISTRIB.IPERGEOM.N +INTERCEPT = INTERCETTA +KURT = CURTOSI +LARGE = GRANDE +LINEST = REGR.LIN +LOGEST = REGR.LOG +LOGNORM.DIST = DISTRIB.LOGNORM.N +LOGNORM.INV = INV.LOGNORM.N +MAX = MAX +MAXA = MAX.VALORI +MAXIFS = MAX.PIÙ.SE +MEDIAN = MEDIANA +MIN = MIN +MINA = MIN.VALORI +MINIFS = MIN.PIÙ.SE +MODE.MULT = MODA.MULT +MODE.SNGL = MODA.SNGL +NEGBINOM.DIST = DISTRIB.BINOM.NEG.N +NORM.DIST = DISTRIB.NORM.N +NORM.INV = INV.NORM.N +NORM.S.DIST = DISTRIB.NORM.ST.N +NORM.S.INV = INV.NORM.S +PEARSON = PEARSON +PERCENTILE.EXC = ESC.PERCENTILE +PERCENTILE.INC = INC.PERCENTILE +PERCENTRANK.EXC = ESC.PERCENT.RANGO +PERCENTRANK.INC = INC.PERCENT.RANGO +PERMUT = PERMUTAZIONE +PERMUTATIONA = PERMUTAZIONE.VALORI +PHI = PHI +POISSON.DIST = DISTRIB.POISSON +PROB = PROBABILITÀ +QUARTILE.EXC = ESC.QUARTILE +QUARTILE.INC = INC.QUARTILE +RANK.AVG = RANGO.MEDIA +RANK.EQ = RANGO.UG +RSQ = RQ +SKEW = ASIMMETRIA +SKEW.P = ASIMMETRIA.P +SLOPE = PENDENZA +SMALL = PICCOLO +STANDARDIZE = NORMALIZZA +STDEV.P = DEV.ST.P +STDEV.S = DEV.ST.C +STDEVA = DEV.ST.VALORI +STDEVPA = DEV.ST.POP.VALORI +STEYX = ERR.STD.YX +T.DIST = DISTRIB.T.N +T.DIST.2T = DISTRIB.T.2T +T.DIST.RT = DISTRIB.T.DS +T.INV = INVT +T.INV.2T = INV.T.2T +T.TEST = TESTT +TREND = TENDENZA +TRIMMEAN = MEDIA.TRONCATA +VAR.P = VAR.P +VAR.S = VAR.C +VARA = VAR.VALORI +VARPA = VAR.POP.VALORI +WEIBULL.DIST = DISTRIB.WEIBULL +Z.TEST = TESTZ + +## +## Funzioni di testo (Text Functions) +## +BAHTTEXT = BAHTTESTO +CHAR = CODICE.CARATT +CLEAN = LIBERA +CODE = CODICE +CONCAT = CONCAT +DOLLAR = VALUTA +EXACT = IDENTICO +FIND = TROVA +FIXED = FISSO +ISTHAIDIGIT = ÈTHAICIFRA +LEFT = SINISTRA +LEN = LUNGHEZZA +LOWER = MINUSC +MID = STRINGA.ESTRAI +NUMBERSTRING = NUMERO.STRINGA +NUMBERVALUE = NUMERO.VALORE +PHONETIC = FURIGANA +PROPER = MAIUSC.INIZ +REPLACE = RIMPIAZZA +REPT = RIPETI +RIGHT = DESTRA +SEARCH = RICERCA +SUBSTITUTE = SOSTITUISCI +T = T +TEXT = TESTO +TEXTJOIN = TESTO.UNISCI +THAIDIGIT = THAICIFRA +THAINUMSOUND = THAINUMSUONO +THAINUMSTRING = THAISZÁMKAR +THAISTRINGLENGTH = THAILUNGSTRINGA +TRIM = ANNULLA.SPAZI +UNICHAR = CARATT.UNI +UNICODE = UNICODE +UPPER = MAIUSC +VALUE = VALORE + +## +## Funzioni Web (Web Functions) +## +ENCODEURL = CODIFICA.URL +FILTERXML = FILTRO.XML +WEBSERVICE = SERVIZIO.WEB + +## +## Funzioni di compatibilità (Compatibility Functions) +## +BETADIST = DISTRIB.BETA +BETAINV = INV.BETA +BINOMDIST = DISTRIB.BINOM +CEILING = ARROTONDA.ECCESSO +CHIDIST = DISTRIB.CHI +CHIINV = INV.CHI +CHITEST = TEST.CHI +CONCATENATE = CONCATENA +CONFIDENCE = CONFIDENZA +COVAR = COVARIANZA +CRITBINOM = CRIT.BINOM +EXPONDIST = DISTRIB.EXP +FDIST = DISTRIB.F +FINV = INV.F +FLOOR = ARROTONDA.DIFETTO +FORECAST = PREVISIONE +FTEST = TEST.F +GAMMADIST = DISTRIB.GAMMA +GAMMAINV = INV.GAMMA +HYPGEOMDIST = DISTRIB.IPERGEOM +LOGINV = INV.LOGNORM +LOGNORMDIST = DISTRIB.LOGNORM +MODE = MODA +NEGBINOMDIST = DISTRIB.BINOM.NEG +NORMDIST = DISTRIB.NORM +NORMINV = INV.NORM +NORMSDIST = DISTRIB.NORM.ST +NORMSINV = INV.NORM.ST +PERCENTILE = PERCENTILE +PERCENTRANK = PERCENT.RANGO +POISSON = POISSON +QUARTILE = QUARTILE +RANK = RANGO +STDEV = DEV.ST +STDEVP = DEV.ST.POP +TDIST = DISTRIB.T +TINV = INV.T +TTEST = TEST.T +VAR = VAR +VARP = VAR.POP +WEIBULL = WEIBULL +ZTEST = TEST.Z diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nb/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nb/config new file mode 100644 index 0000000..a7f3be1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nb/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Norsk Bokmål (Norwegian Bokmål) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL +DIV0 +VALUE = #VERDI! +REF +NAME = #NAVN? +NUM +NA = #N/D diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nb/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nb/functions new file mode 100644 index 0000000..d352e1f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nb/functions @@ -0,0 +1,539 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Norsk Bokmål (Norwegian Bokmål) +## +############################################################ + + +## +## Kubefunksjoner (Cube Functions) +## +CUBEKPIMEMBER = KUBEKPIMEDLEM +CUBEMEMBER = KUBEMEDLEM +CUBEMEMBERPROPERTY = KUBEMEDLEMEGENSKAP +CUBERANKEDMEMBER = KUBERANGERTMEDLEM +CUBESET = KUBESETT +CUBESETCOUNT = KUBESETTANTALL +CUBEVALUE = KUBEVERDI + +## +## Databasefunksjoner (Database Functions) +## +DAVERAGE = DGJENNOMSNITT +DCOUNT = DANTALL +DCOUNTA = DANTALLA +DGET = DHENT +DMAX = DMAKS +DMIN = DMIN +DPRODUCT = DPRODUKT +DSTDEV = DSTDAV +DSTDEVP = DSTDAVP +DSUM = DSUMMER +DVAR = DVARIANS +DVARP = DVARIANSP + +## +## Dato- og tidsfunksjoner (Date & Time Functions) +## +DATE = DATO +DATEDIF = DATODIFF +DATESTRING = DATOSTRENG +DATEVALUE = DATOVERDI +DAY = DAG +DAYS = DAGER +DAYS360 = DAGER360 +EDATE = DAG.ETTER +EOMONTH = MÅNEDSSLUTT +HOUR = TIME +ISOWEEKNUM = ISOUKENR +MINUTE = MINUTT +MONTH = MÅNED +NETWORKDAYS = NETT.ARBEIDSDAGER +NETWORKDAYS.INTL = NETT.ARBEIDSDAGER.INTL +NOW = NÅ +SECOND = SEKUND +THAIDAYOFWEEK = THAIUKEDAG +THAIMONTHOFYEAR = THAIMÅNED +THAIYEAR = THAIÅR +TIME = TID +TIMEVALUE = TIDSVERDI +TODAY = IDAG +WEEKDAY = UKEDAG +WEEKNUM = UKENR +WORKDAY = ARBEIDSDAG +WORKDAY.INTL = ARBEIDSDAG.INTL +YEAR = ÅR +YEARFRAC = ÅRDEL + +## +## Tekniske funksjoner (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BINTILDES +BIN2HEX = BINTILHEKS +BIN2OCT = BINTILOKT +BITAND = BITOG +BITLSHIFT = BITVFORSKYV +BITOR = BITELLER +BITRSHIFT = BITHFORSKYV +BITXOR = BITEKSKLUSIVELLER +COMPLEX = KOMPLEKS +CONVERT = KONVERTER +DEC2BIN = DESTILBIN +DEC2HEX = DESTILHEKS +DEC2OCT = DESTILOKT +DELTA = DELTA +ERF = FEILF +ERF.PRECISE = FEILF.PRESIS +ERFC = FEILFK +ERFC.PRECISE = FEILFK.PRESIS +GESTEP = GRENSEVERDI +HEX2BIN = HEKSTILBIN +HEX2DEC = HEKSTILDES +HEX2OCT = HEKSTILOKT +IMABS = IMABS +IMAGINARY = IMAGINÆR +IMARGUMENT = IMARGUMENT +IMCONJUGATE = IMKONJUGERT +IMCOS = IMCOS +IMCOSH = IMCOSH +IMCOT = IMCOT +IMCSC = IMCSC +IMCSCH = IMCSCH +IMDIV = IMDIV +IMEXP = IMEKSP +IMLN = IMLN +IMLOG10 = IMLOG10 +IMLOG2 = IMLOG2 +IMPOWER = IMOPPHØY +IMPRODUCT = IMPRODUKT +IMREAL = IMREELL +IMSEC = IMSEC +IMSECH = IMSECH +IMSIN = IMSIN +IMSINH = IMSINH +IMSQRT = IMROT +IMSUB = IMSUB +IMSUM = IMSUMMER +IMTAN = IMTAN +OCT2BIN = OKTTILBIN +OCT2DEC = OKTTILDES +OCT2HEX = OKTTILHEKS + +## +## Økonomiske funksjoner (Financial Functions) +## +ACCRINT = PÅLØPT.PERIODISK.RENTE +ACCRINTM = PÅLØPT.FORFALLSRENTE +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = OBLIG.DAGER.FF +COUPDAYS = OBLIG.DAGER +COUPDAYSNC = OBLIG.DAGER.NF +COUPNCD = OBLIG.DAGER.EF +COUPNUM = OBLIG.ANTALL +COUPPCD = OBLIG.DAG.FORRIGE +CUMIPMT = SAMLET.RENTE +CUMPRINC = SAMLET.HOVEDSTOL +DB = DAVSKR +DDB = DEGRAVS +DISC = DISKONTERT +DOLLARDE = DOLLARDE +DOLLARFR = DOLLARBR +DURATION = VARIGHET +EFFECT = EFFEKTIV.RENTE +FV = SLUTTVERDI +FVSCHEDULE = SVPLAN +INTRATE = RENTESATS +IPMT = RAVDRAG +IRR = IR +ISPMT = ER.AVDRAG +MDURATION = MVARIGHET +MIRR = MODIR +NOMINAL = NOMINELL +NPER = PERIODER +NPV = NNV +ODDFPRICE = AVVIKFP.PRIS +ODDFYIELD = AVVIKFP.AVKASTNING +ODDLPRICE = AVVIKSP.PRIS +ODDLYIELD = AVVIKSP.AVKASTNING +PDURATION = PVARIGHET +PMT = AVDRAG +PPMT = AMORT +PRICE = PRIS +PRICEDISC = PRIS.DISKONTERT +PRICEMAT = PRIS.FORFALL +PV = NÅVERDI +RATE = RENTE +RECEIVED = MOTTATT.AVKAST +RRI = REALISERT.AVKASTNING +SLN = LINAVS +SYD = ÅRSAVS +TBILLEQ = TBILLEKV +TBILLPRICE = TBILLPRIS +TBILLYIELD = TBILLAVKASTNING +VDB = VERDIAVS +XIRR = XIR +XNPV = XNNV +YIELD = AVKAST +YIELDDISC = AVKAST.DISKONTERT +YIELDMAT = AVKAST.FORFALL + +## +## Informasjonsfunksjoner (Information Functions) +## +CELL = CELLE +ERROR.TYPE = FEIL.TYPE +INFO = INFO +ISBLANK = ERTOM +ISERR = ERF +ISERROR = ERFEIL +ISEVEN = ERPARTALL +ISFORMULA = ERFORMEL +ISLOGICAL = ERLOGISK +ISNA = ERIT +ISNONTEXT = ERIKKETEKST +ISNUMBER = ERTALL +ISODD = ERODDE +ISREF = ERREF +ISTEXT = ERTEKST +N = N +NA = IT +SHEET = ARK +SHEETS = ANTALL.ARK +TYPE = VERDITYPE + +## +## Logiske funksjoner (Logical Functions) +## +AND = OG +FALSE = USANN +IF = HVIS +IFERROR = HVISFEIL +IFNA = HVIS.IT +IFS = HVIS.SETT +NOT = IKKE +OR = ELLER +SWITCH = BRYTER +TRUE = SANN +XOR = EKSKLUSIVELLER + +## +## Oppslag- og referansefunksjoner (Lookup & Reference Functions) +## +ADDRESS = ADRESSE +AREAS = OMRÅDER +CHOOSE = VELG +COLUMN = KOLONNE +COLUMNS = KOLONNER +FORMULATEXT = FORMELTEKST +GETPIVOTDATA = HENTPIVOTDATA +HLOOKUP = FINN.KOLONNE +HYPERLINK = HYPERKOBLING +INDEX = INDEKS +INDIRECT = INDIREKTE +LOOKUP = SLÅ.OPP +MATCH = SAMMENLIGNE +OFFSET = FORSKYVNING +ROW = RAD +ROWS = RADER +RTD = RTD +TRANSPOSE = TRANSPONER +VLOOKUP = FINN.RAD +*RC = RK + +## +## Matematikk- og trigonometrifunksjoner (Math & Trig Functions) +## +ABS = ABS +ACOS = ARCCOS +ACOSH = ARCCOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = MENGDE +ARABIC = ARABISK +ASIN = ARCSIN +ASINH = ARCSINH +ATAN = ARCTAN +ATAN2 = ARCTAN2 +ATANH = ARCTANH +BASE = GRUNNTALL +CEILING.MATH = AVRUND.GJELDENDE.MULTIPLUM.OPP.MATEMATISK +CEILING.PRECISE = AVRUND.GJELDENDE.MULTIPLUM.PRESIS +COMBIN = KOMBINASJON +COMBINA = KOMBINASJONA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DESIMAL +DEGREES = GRADER +ECMA.CEILING = ECMA.AVRUND.GJELDENDE.MULTIPLUM +EVEN = AVRUND.TIL.PARTALL +EXP = EKSP +FACT = FAKULTET +FACTDOUBLE = DOBBELFAKT +FLOOR.MATH = AVRUND.GJELDENDE.MULTIPLUM.NED.MATEMATISK +FLOOR.PRECISE = AVRUND.GJELDENDE.MULTIPLUM.NED.PRESIS +GCD = SFF +INT = HELTALL +ISO.CEILING = ISO.AVRUND.GJELDENDE.MULTIPLUM +LCM = MFM +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MDETERM +MINVERSE = MINVERS +MMULT = MMULT +MOD = REST +MROUND = MRUND +MULTINOMIAL = MULTINOMINELL +MUNIT = MENHET +ODD = AVRUND.TIL.ODDETALL +PI = PI +POWER = OPPHØYD.I +PRODUCT = PRODUKT +QUOTIENT = KVOTIENT +RADIANS = RADIANER +RAND = TILFELDIG +RANDBETWEEN = TILFELDIGMELLOM +ROMAN = ROMERTALL +ROUND = AVRUND +ROUNDBAHTDOWN = RUNDAVBAHTNEDOVER +ROUNDBAHTUP = RUNDAVBAHTOPPOVER +ROUNDDOWN = AVRUND.NED +ROUNDUP = AVRUND.OPP +SEC = SEC +SECH = SECH +SERIESSUM = SUMMER.REKKE +SIGN = FORTEGN +SIN = SIN +SINH = SINH +SQRT = ROT +SQRTPI = ROTPI +SUBTOTAL = DELSUM +SUM = SUMMER +SUMIF = SUMMERHVIS +SUMIFS = SUMMER.HVIS.SETT +SUMPRODUCT = SUMMERPRODUKT +SUMSQ = SUMMERKVADRAT +SUMX2MY2 = SUMMERX2MY2 +SUMX2PY2 = SUMMERX2PY2 +SUMXMY2 = SUMMERXMY2 +TAN = TAN +TANH = TANH +TRUNC = AVKORT + +## +## Statistiske funksjoner (Statistical Functions) +## +AVEDEV = GJENNOMSNITTSAVVIK +AVERAGE = GJENNOMSNITT +AVERAGEA = GJENNOMSNITTA +AVERAGEIF = GJENNOMSNITTHVIS +AVERAGEIFS = GJENNOMSNITT.HVIS.SETT +BETA.DIST = BETA.FORDELING.N +BETA.INV = BETA.INV +BINOM.DIST = BINOM.FORDELING.N +BINOM.DIST.RANGE = BINOM.FORDELING.OMRÅDE +BINOM.INV = BINOM.INV +CHISQ.DIST = KJIKVADRAT.FORDELING +CHISQ.DIST.RT = KJIKVADRAT.FORDELING.H +CHISQ.INV = KJIKVADRAT.INV +CHISQ.INV.RT = KJIKVADRAT.INV.H +CHISQ.TEST = KJIKVADRAT.TEST +CONFIDENCE.NORM = KONFIDENS.NORM +CONFIDENCE.T = KONFIDENS.T +CORREL = KORRELASJON +COUNT = ANTALL +COUNTA = ANTALLA +COUNTBLANK = TELLBLANKE +COUNTIF = ANTALL.HVIS +COUNTIFS = ANTALL.HVIS.SETT +COVARIANCE.P = KOVARIANS.P +COVARIANCE.S = KOVARIANS.S +DEVSQ = AVVIK.KVADRERT +EXPON.DIST = EKSP.FORDELING.N +F.DIST = F.FORDELING +F.DIST.RT = F.FORDELING.H +F.INV = F.INV +F.INV.RT = F.INV.H +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = PROGNOSE.ETS +FORECAST.ETS.CONFINT = PROGNOSE.ETS.CONFINT +FORECAST.ETS.SEASONALITY = PROGNOSE.ETS.SESONGAVHENGIGHET +FORECAST.ETS.STAT = PROGNOSE.ETS.STAT +FORECAST.LINEAR = PROGNOSE.LINEÆR +FREQUENCY = FREKVENS +GAMMA = GAMMA +GAMMA.DIST = GAMMA.FORDELING +GAMMA.INV = GAMMA.INV +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.PRESIS +GAUSS = GAUSS +GEOMEAN = GJENNOMSNITT.GEOMETRISK +GROWTH = VEKST +HARMEAN = GJENNOMSNITT.HARMONISK +HYPGEOM.DIST = HYPGEOM.FORDELING.N +INTERCEPT = SKJÆRINGSPUNKT +KURT = KURT +LARGE = N.STØRST +LINEST = RETTLINJE +LOGEST = KURVE +LOGNORM.DIST = LOGNORM.FORDELING +LOGNORM.INV = LOGNORM.INV +MAX = STØRST +MAXA = MAKSA +MAXIFS = MAKS.HVIS.SETT +MEDIAN = MEDIAN +MIN = MIN +MINA = MINA +MINIFS = MIN.HVIS.SETT +MODE.MULT = MODUS.MULT +MODE.SNGL = MODUS.SNGL +NEGBINOM.DIST = NEGBINOM.FORDELING.N +NORM.DIST = NORM.FORDELING +NORM.INV = NORM.INV +NORM.S.DIST = NORM.S.FORDELING +NORM.S.INV = NORM.S.INV +PEARSON = PEARSON +PERCENTILE.EXC = PERSENTIL.EKS +PERCENTILE.INC = PERSENTIL.INK +PERCENTRANK.EXC = PROSENTDEL.EKS +PERCENTRANK.INC = PROSENTDEL.INK +PERMUT = PERMUTER +PERMUTATIONA = PERMUTASJONA +PHI = PHI +POISSON.DIST = POISSON.FORDELING +PROB = SANNSYNLIG +QUARTILE.EXC = KVARTIL.EKS +QUARTILE.INC = KVARTIL.INK +RANK.AVG = RANG.GJSN +RANK.EQ = RANG.EKV +RSQ = RKVADRAT +SKEW = SKJEVFORDELING +SKEW.P = SKJEVFORDELING.P +SLOPE = STIGNINGSTALL +SMALL = N.MINST +STANDARDIZE = NORMALISER +STDEV.P = STDAV.P +STDEV.S = STDAV.S +STDEVA = STDAVVIKA +STDEVPA = STDAVVIKPA +STEYX = STANDARDFEIL +T.DIST = T.FORDELING +T.DIST.2T = T.FORDELING.2T +T.DIST.RT = T.FORDELING.H +T.INV = T.INV +T.INV.2T = T.INV.2T +T.TEST = T.TEST +TREND = TREND +TRIMMEAN = TRIMMET.GJENNOMSNITT +VAR.P = VARIANS.P +VAR.S = VARIANS.S +VARA = VARIANSA +VARPA = VARIANSPA +WEIBULL.DIST = WEIBULL.DIST.N +Z.TEST = Z.TEST + +## +## Tekstfunksjoner (Text Functions) +## +ASC = STIGENDE +BAHTTEXT = BAHTTEKST +CHAR = TEGNKODE +CLEAN = RENSK +CODE = KODE +CONCAT = KJED.SAMMEN +DOLLAR = VALUTA +EXACT = EKSAKT +FIND = FINN +FIXED = FASTSATT +ISTHAIDIGIT = ERTHAISIFFER +LEFT = VENSTRE +LEN = LENGDE +LOWER = SMÅ +MID = DELTEKST +NUMBERSTRING = TALLSTRENG +NUMBERVALUE = TALLVERDI +PHONETIC = FURIGANA +PROPER = STOR.FORBOKSTAV +REPLACE = ERSTATT +REPT = GJENTA +RIGHT = HØYRE +SEARCH = SØK +SUBSTITUTE = BYTT.UT +T = T +TEXT = TEKST +TEXTJOIN = TEKST.KOMBINER +THAIDIGIT = THAISIFFER +THAINUMSOUND = THAINUMLYD +THAINUMSTRING = THAINUMSTRENG +THAISTRINGLENGTH = THAISTRENGLENGDE +TRIM = TRIMME +UNICHAR = UNICODETEGN +UNICODE = UNICODE +UPPER = STORE +VALUE = VERDI + +## +## Nettfunksjoner (Web Functions) +## +ENCODEURL = URL.KODE +FILTERXML = FILTRERXML +WEBSERVICE = NETTJENESTE + +## +## Kompatibilitetsfunksjoner (Compatibility Functions) +## +BETADIST = BETA.FORDELING +BETAINV = INVERS.BETA.FORDELING +BINOMDIST = BINOM.FORDELING +CEILING = AVRUND.GJELDENDE.MULTIPLUM +CHIDIST = KJI.FORDELING +CHIINV = INVERS.KJI.FORDELING +CHITEST = KJI.TEST +CONCATENATE = KJEDE.SAMMEN +CONFIDENCE = KONFIDENS +COVAR = KOVARIANS +CRITBINOM = GRENSE.BINOM +EXPONDIST = EKSP.FORDELING +FDIST = FFORDELING +FINV = FFORDELING.INVERS +FLOOR = AVRUND.GJELDENDE.MULTIPLUM.NED +FORECAST = PROGNOSE +FTEST = FTEST +GAMMADIST = GAMMAFORDELING +GAMMAINV = GAMMAINV +HYPGEOMDIST = HYPGEOM.FORDELING +LOGINV = LOGINV +LOGNORMDIST = LOGNORMFORD +MODE = MODUS +NEGBINOMDIST = NEGBINOM.FORDELING +NORMDIST = NORMALFORDELING +NORMINV = NORMINV +NORMSDIST = NORMSFORDELING +NORMSINV = NORMSINV +PERCENTILE = PERSENTIL +PERCENTRANK = PROSENTDEL +POISSON = POISSON +QUARTILE = KVARTIL +RANK = RANG +STDEV = STDAV +STDEVP = STDAVP +TDIST = TFORDELING +TINV = TINV +TTEST = TTEST +VAR = VARIANS +VARP = VARIANSP +WEIBULL = WEIBULL.FORDELING +ZTEST = ZTEST diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nl/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nl/config new file mode 100644 index 0000000..370567a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nl/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Nederlands (Dutch) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #LEEG! +DIV0 = #DEEL/0! +VALUE = #WAARDE! +REF = #VERW! +NAME = #NAAM? +NUM = #GETAL! +NA = #N/B diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nl/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nl/functions new file mode 100644 index 0000000..7fd661b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/nl/functions @@ -0,0 +1,538 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Nederlands (Dutch) +## +############################################################ + + +## +## Kubusfuncties (Cube Functions) +## +CUBEKPIMEMBER = KUBUSKPILID +CUBEMEMBER = KUBUSLID +CUBEMEMBERPROPERTY = KUBUSLIDEIGENSCHAP +CUBERANKEDMEMBER = KUBUSGERANGSCHIKTLID +CUBESET = KUBUSSET +CUBESETCOUNT = KUBUSSETAANTAL +CUBEVALUE = KUBUSWAARDE + +## +## Databasefuncties (Database Functions) +## +DAVERAGE = DBGEMIDDELDE +DCOUNT = DBAANTAL +DCOUNTA = DBAANTALC +DGET = DBLEZEN +DMAX = DBMAX +DMIN = DBMIN +DPRODUCT = DBPRODUCT +DSTDEV = DBSTDEV +DSTDEVP = DBSTDEVP +DSUM = DBSOM +DVAR = DBVAR +DVARP = DBVARP + +## +## Datum- en tijdfuncties (Date & Time Functions) +## +DATE = DATUM +DATEDIF = DATUMVERSCHIL +DATESTRING = DATUMNOTATIE +DATEVALUE = DATUMWAARDE +DAY = DAG +DAYS = DAGEN +DAYS360 = DAGEN360 +EDATE = ZELFDE.DAG +EOMONTH = LAATSTE.DAG +HOUR = UUR +ISOWEEKNUM = ISO.WEEKNUMMER +MINUTE = MINUUT +MONTH = MAAND +NETWORKDAYS = NETTO.WERKDAGEN +NETWORKDAYS.INTL = NETWERKDAGEN.INTL +NOW = NU +SECOND = SECONDE +THAIDAYOFWEEK = THAIS.WEEKDAG +THAIMONTHOFYEAR = THAIS.MAAND.VAN.JAAR +THAIYEAR = THAIS.JAAR +TIME = TIJD +TIMEVALUE = TIJDWAARDE +TODAY = VANDAAG +WEEKDAY = WEEKDAG +WEEKNUM = WEEKNUMMER +WORKDAY = WERKDAG +WORKDAY.INTL = WERKDAG.INTL +YEAR = JAAR +YEARFRAC = JAAR.DEEL + +## +## Technische functies (Engineering Functions) +## +BESSELI = BESSEL.I +BESSELJ = BESSEL.J +BESSELK = BESSEL.K +BESSELY = BESSEL.Y +BIN2DEC = BIN.N.DEC +BIN2HEX = BIN.N.HEX +BIN2OCT = BIN.N.OCT +BITAND = BIT.EN +BITLSHIFT = BIT.VERSCHUIF.LINKS +BITOR = BIT.OF +BITRSHIFT = BIT.VERSCHUIF.RECHTS +BITXOR = BIT.EX.OF +COMPLEX = COMPLEX +CONVERT = CONVERTEREN +DEC2BIN = DEC.N.BIN +DEC2HEX = DEC.N.HEX +DEC2OCT = DEC.N.OCT +DELTA = DELTA +ERF = FOUTFUNCTIE +ERF.PRECISE = FOUTFUNCTIE.NAUWKEURIG +ERFC = FOUT.COMPLEMENT +ERFC.PRECISE = FOUT.COMPLEMENT.NAUWKEURIG +GESTEP = GROTER.DAN +HEX2BIN = HEX.N.BIN +HEX2DEC = HEX.N.DEC +HEX2OCT = HEX.N.OCT +IMABS = C.ABS +IMAGINARY = C.IM.DEEL +IMARGUMENT = C.ARGUMENT +IMCONJUGATE = C.TOEGEVOEGD +IMCOS = C.COS +IMCOSH = C.COSH +IMCOT = C.COT +IMCSC = C.COSEC +IMCSCH = C.COSECH +IMDIV = C.QUOTIENT +IMEXP = C.EXP +IMLN = C.LN +IMLOG10 = C.LOG10 +IMLOG2 = C.LOG2 +IMPOWER = C.MACHT +IMPRODUCT = C.PRODUCT +IMREAL = C.REEEL.DEEL +IMSEC = C.SEC +IMSECH = C.SECH +IMSIN = C.SIN +IMSINH = C.SINH +IMSQRT = C.WORTEL +IMSUB = C.VERSCHIL +IMSUM = C.SOM +IMTAN = C.TAN +OCT2BIN = OCT.N.BIN +OCT2DEC = OCT.N.DEC +OCT2HEX = OCT.N.HEX + +## +## Financiële functies (Financial Functions) +## +ACCRINT = SAMENG.RENTE +ACCRINTM = SAMENG.RENTE.V +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = COUP.DAGEN.BB +COUPDAYS = COUP.DAGEN +COUPDAYSNC = COUP.DAGEN.VV +COUPNCD = COUP.DATUM.NB +COUPNUM = COUP.AANTAL +COUPPCD = COUP.DATUM.VB +CUMIPMT = CUM.RENTE +CUMPRINC = CUM.HOOFDSOM +DB = DB +DDB = DDB +DISC = DISCONTO +DOLLARDE = EURO.DE +DOLLARFR = EURO.BR +DURATION = DUUR +EFFECT = EFFECT.RENTE +FV = TW +FVSCHEDULE = TOEK.WAARDE2 +INTRATE = RENTEPERCENTAGE +IPMT = IBET +IRR = IR +ISPMT = ISBET +MDURATION = AANG.DUUR +MIRR = GIR +NOMINAL = NOMINALE.RENTE +NPER = NPER +NPV = NHW +ODDFPRICE = AFW.ET.PRIJS +ODDFYIELD = AFW.ET.REND +ODDLPRICE = AFW.LT.PRIJS +ODDLYIELD = AFW.LT.REND +PDURATION = PDUUR +PMT = BET +PPMT = PBET +PRICE = PRIJS.NOM +PRICEDISC = PRIJS.DISCONTO +PRICEMAT = PRIJS.VERVALDAG +PV = HW +RATE = RENTE +RECEIVED = OPBRENGST +RRI = RRI +SLN = LIN.AFSCHR +SYD = SYD +TBILLEQ = SCHATK.OBL +TBILLPRICE = SCHATK.PRIJS +TBILLYIELD = SCHATK.REND +VDB = VDB +XIRR = IR.SCHEMA +XNPV = NHW2 +YIELD = RENDEMENT +YIELDDISC = REND.DISCONTO +YIELDMAT = REND.VERVAL + +## +## Informatiefuncties (Information Functions) +## +CELL = CEL +ERROR.TYPE = TYPE.FOUT +INFO = INFO +ISBLANK = ISLEEG +ISERR = ISFOUT2 +ISERROR = ISFOUT +ISEVEN = IS.EVEN +ISFORMULA = ISFORMULE +ISLOGICAL = ISLOGISCH +ISNA = ISNB +ISNONTEXT = ISGEENTEKST +ISNUMBER = ISGETAL +ISODD = IS.ONEVEN +ISREF = ISVERWIJZING +ISTEXT = ISTEKST +N = N +NA = NB +SHEET = BLAD +SHEETS = BLADEN +TYPE = TYPE + +## +## Logische functies (Logical Functions) +## +AND = EN +FALSE = ONWAAR +IF = ALS +IFERROR = ALS.FOUT +IFNA = ALS.NB +IFS = ALS.VOORWAARDEN +NOT = NIET +OR = OF +SWITCH = SCHAKELEN +TRUE = WAAR +XOR = EX.OF + +## +## Zoek- en verwijzingsfuncties (Lookup & Reference Functions) +## +ADDRESS = ADRES +AREAS = BEREIKEN +CHOOSE = KIEZEN +COLUMN = KOLOM +COLUMNS = KOLOMMEN +FORMULATEXT = FORMULETEKST +GETPIVOTDATA = DRAAITABEL.OPHALEN +HLOOKUP = HORIZ.ZOEKEN +HYPERLINK = HYPERLINK +INDEX = INDEX +INDIRECT = INDIRECT +LOOKUP = ZOEKEN +MATCH = VERGELIJKEN +OFFSET = VERSCHUIVING +ROW = RIJ +ROWS = RIJEN +RTD = RTG +TRANSPOSE = TRANSPONEREN +VLOOKUP = VERT.ZOEKEN +*RC = RK + +## +## Wiskundige en trigonometrische functies (Math & Trig Functions) +## +ABS = ABS +ACOS = BOOGCOS +ACOSH = BOOGCOSH +ACOT = BOOGCOT +ACOTH = BOOGCOTH +AGGREGATE = AGGREGAAT +ARABIC = ARABISCH +ASIN = BOOGSIN +ASINH = BOOGSINH +ATAN = BOOGTAN +ATAN2 = BOOGTAN2 +ATANH = BOOGTANH +BASE = BASIS +CEILING.MATH = AFRONDEN.BOVEN.WISK +CEILING.PRECISE = AFRONDEN.BOVEN.NAUWKEURIG +COMBIN = COMBINATIES +COMBINA = COMBIN.A +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = COSEC +CSCH = COSECH +DECIMAL = DECIMAAL +DEGREES = GRADEN +ECMA.CEILING = ECMA.AFRONDEN.BOVEN +EVEN = EVEN +EXP = EXP +FACT = FACULTEIT +FACTDOUBLE = DUBBELE.FACULTEIT +FLOOR.MATH = AFRONDEN.BENEDEN.WISK +FLOOR.PRECISE = AFRONDEN.BENEDEN.NAUWKEURIG +GCD = GGD +INT = INTEGER +ISO.CEILING = ISO.AFRONDEN.BOVEN +LCM = KGV +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = DETERMINANTMAT +MINVERSE = INVERSEMAT +MMULT = PRODUCTMAT +MOD = REST +MROUND = AFRONDEN.N.VEELVOUD +MULTINOMIAL = MULTINOMIAAL +MUNIT = EENHEIDMAT +ODD = ONEVEN +PI = PI +POWER = MACHT +PRODUCT = PRODUCT +QUOTIENT = QUOTIENT +RADIANS = RADIALEN +RAND = ASELECT +RANDBETWEEN = ASELECTTUSSEN +ROMAN = ROMEINS +ROUND = AFRONDEN +ROUNDBAHTDOWN = BAHT.AFR.NAAR.BENEDEN +ROUNDBAHTUP = BAHT.AFR.NAAR.BOVEN +ROUNDDOWN = AFRONDEN.NAAR.BENEDEN +ROUNDUP = AFRONDEN.NAAR.BOVEN +SEC = SEC +SECH = SECH +SERIESSUM = SOM.MACHTREEKS +SIGN = POS.NEG +SIN = SIN +SINH = SINH +SQRT = WORTEL +SQRTPI = WORTEL.PI +SUBTOTAL = SUBTOTAAL +SUM = SOM +SUMIF = SOM.ALS +SUMIFS = SOMMEN.ALS +SUMPRODUCT = SOMPRODUCT +SUMSQ = KWADRATENSOM +SUMX2MY2 = SOM.X2MINY2 +SUMX2PY2 = SOM.X2PLUSY2 +SUMXMY2 = SOM.XMINY.2 +TAN = TAN +TANH = TANH +TRUNC = GEHEEL + +## +## Statistische functies (Statistical Functions) +## +AVEDEV = GEM.DEVIATIE +AVERAGE = GEMIDDELDE +AVERAGEA = GEMIDDELDEA +AVERAGEIF = GEMIDDELDE.ALS +AVERAGEIFS = GEMIDDELDEN.ALS +BETA.DIST = BETA.VERD +BETA.INV = BETA.INV +BINOM.DIST = BINOM.VERD +BINOM.DIST.RANGE = BINOM.VERD.BEREIK +BINOM.INV = BINOMIALE.INV +CHISQ.DIST = CHIKW.VERD +CHISQ.DIST.RT = CHIKW.VERD.RECHTS +CHISQ.INV = CHIKW.INV +CHISQ.INV.RT = CHIKW.INV.RECHTS +CHISQ.TEST = CHIKW.TEST +CONFIDENCE.NORM = VERTROUWELIJKHEID.NORM +CONFIDENCE.T = VERTROUWELIJKHEID.T +CORREL = CORRELATIE +COUNT = AANTAL +COUNTA = AANTALARG +COUNTBLANK = AANTAL.LEGE.CELLEN +COUNTIF = AANTAL.ALS +COUNTIFS = AANTALLEN.ALS +COVARIANCE.P = COVARIANTIE.P +COVARIANCE.S = COVARIANTIE.S +DEVSQ = DEV.KWAD +EXPON.DIST = EXPON.VERD.N +F.DIST = F.VERD +F.DIST.RT = F.VERD.RECHTS +F.INV = F.INV +F.INV.RT = F.INV.RECHTS +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHER.INV +FORECAST.ETS = VOORSPELLEN.ETS +FORECAST.ETS.CONFINT = VOORSPELLEN.ETS.CONFINT +FORECAST.ETS.SEASONALITY = VOORSPELLEN.ETS.SEASONALITY +FORECAST.ETS.STAT = FORECAST.ETS.STAT +FORECAST.LINEAR = VOORSPELLEN.LINEAR +FREQUENCY = INTERVAL +GAMMA = GAMMA +GAMMA.DIST = GAMMA.VERD.N +GAMMA.INV = GAMMA.INV.N +GAMMALN = GAMMA.LN +GAMMALN.PRECISE = GAMMA.LN.NAUWKEURIG +GAUSS = GAUSS +GEOMEAN = MEETK.GEM +GROWTH = GROEI +HARMEAN = HARM.GEM +HYPGEOM.DIST = HYPGEOM.VERD +INTERCEPT = SNIJPUNT +KURT = KURTOSIS +LARGE = GROOTSTE +LINEST = LIJNSCH +LOGEST = LOGSCH +LOGNORM.DIST = LOGNORM.VERD +LOGNORM.INV = LOGNORM.INV +MAX = MAX +MAXA = MAXA +MAXIFS = MAX.ALS.VOORWAARDEN +MEDIAN = MEDIAAN +MIN = MIN +MINA = MINA +MINIFS = MIN.ALS.VOORWAARDEN +MODE.MULT = MODUS.MEERV +MODE.SNGL = MODUS.ENKELV +NEGBINOM.DIST = NEGBINOM.VERD +NORM.DIST = NORM.VERD.N +NORM.INV = NORM.INV.N +NORM.S.DIST = NORM.S.VERD +NORM.S.INV = NORM.S.INV +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTIEL.EXC +PERCENTILE.INC = PERCENTIEL.INC +PERCENTRANK.EXC = PROCENTRANG.EXC +PERCENTRANK.INC = PROCENTRANG.INC +PERMUT = PERMUTATIES +PERMUTATIONA = PERMUTATIE.A +PHI = PHI +POISSON.DIST = POISSON.VERD +PROB = KANS +QUARTILE.EXC = KWARTIEL.EXC +QUARTILE.INC = KWARTIEL.INC +RANK.AVG = RANG.GEMIDDELDE +RANK.EQ = RANG.GELIJK +RSQ = R.KWADRAAT +SKEW = SCHEEFHEID +SKEW.P = SCHEEFHEID.P +SLOPE = RICHTING +SMALL = KLEINSTE +STANDARDIZE = NORMALISEREN +STDEV.P = STDEV.P +STDEV.S = STDEV.S +STDEVA = STDEVA +STDEVPA = STDEVPA +STEYX = STAND.FOUT.YX +T.DIST = T.DIST +T.DIST.2T = T.VERD.2T +T.DIST.RT = T.VERD.RECHTS +T.INV = T.INV +T.INV.2T = T.INV.2T +T.TEST = T.TEST +TREND = TREND +TRIMMEAN = GETRIMD.GEM +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = WEIBULL.VERD +Z.TEST = Z.TEST + +## +## Tekstfuncties (Text Functions) +## +BAHTTEXT = BAHT.TEKST +CHAR = TEKEN +CLEAN = WISSEN.CONTROL +CODE = CODE +CONCAT = TEKST.SAMENV +DOLLAR = EURO +EXACT = GELIJK +FIND = VIND.ALLES +FIXED = VAST +ISTHAIDIGIT = IS.THAIS.CIJFER +LEFT = LINKS +LEN = LENGTE +LOWER = KLEINE.LETTERS +MID = DEEL +NUMBERSTRING = GETALNOTATIE +NUMBERVALUE = NUMERIEKE.WAARDE +PHONETIC = FONETISCH +PROPER = BEGINLETTERS +REPLACE = VERVANGEN +REPT = HERHALING +RIGHT = RECHTS +SEARCH = VIND.SPEC +SUBSTITUTE = SUBSTITUEREN +T = T +TEXT = TEKST +TEXTJOIN = TEKST.COMBINEREN +THAIDIGIT = THAIS.CIJFER +THAINUMSOUND = THAIS.GETAL.GELUID +THAINUMSTRING = THAIS.GETAL.REEKS +THAISTRINGLENGTH = THAIS.REEKS.LENGTE +TRIM = SPATIES.WISSEN +UNICHAR = UNITEKEN +UNICODE = UNICODE +UPPER = HOOFDLETTERS +VALUE = WAARDE + +## +## Webfuncties (Web Functions) +## +ENCODEURL = URL.CODEREN +FILTERXML = XML.FILTEREN +WEBSERVICE = WEBSERVICE + +## +## Compatibiliteitsfuncties (Compatibility Functions) +## +BETADIST = BETAVERD +BETAINV = BETAINV +BINOMDIST = BINOMIALE.VERD +CEILING = AFRONDEN.BOVEN +CHIDIST = CHI.KWADRAAT +CHIINV = CHI.KWADRAAT.INV +CHITEST = CHI.TOETS +CONCATENATE = TEKST.SAMENVOEGEN +CONFIDENCE = BETROUWBAARHEID +COVAR = COVARIANTIE +CRITBINOM = CRIT.BINOM +EXPONDIST = EXPON.VERD +FDIST = F.VERDELING +FINV = F.INVERSE +FLOOR = AFRONDEN.BENEDEN +FORECAST = VOORSPELLEN +FTEST = F.TOETS +GAMMADIST = GAMMA.VERD +GAMMAINV = GAMMA.INV +HYPGEOMDIST = HYPERGEO.VERD +LOGINV = LOG.NORM.INV +LOGNORMDIST = LOG.NORM.VERD +MODE = MODUS +NEGBINOMDIST = NEG.BINOM.VERD +NORMDIST = NORM.VERD +NORMINV = NORM.INV +NORMSDIST = STAND.NORM.VERD +NORMSINV = STAND.NORM.INV +PERCENTILE = PERCENTIEL +PERCENTRANK = PERCENT.RANG +POISSON = POISSON +QUARTILE = KWARTIEL +RANK = RANG +STDEV = STDEV +STDEVP = STDEVP +TDIST = T.VERD +TINV = TINV +TTEST = T.TOETS +VAR = VAR +VARP = VARP +WEIBULL = WEIBULL +ZTEST = Z.TOETS diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pl/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pl/config new file mode 100644 index 0000000..1d8468b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pl/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Jezyk polski (Polish) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #ZERO! +DIV0 = #DZIEL/0! +VALUE = #ARG! +REF = #ADR! +NAME = #NAZWA? +NUM = #LICZBA! +NA = #N/D! diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pl/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pl/functions new file mode 100644 index 0000000..d1b43b2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pl/functions @@ -0,0 +1,536 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Jezyk polski (Polish) +## +############################################################ + + +## +## Funkcje baz danych (Cube Functions) +## +CUBEKPIMEMBER = ELEMENT.KPI.MODUŁU +CUBEMEMBER = ELEMENT.MODUŁU +CUBEMEMBERPROPERTY = WŁAŚCIWOŚĆ.ELEMENTU.MODUŁU +CUBERANKEDMEMBER = USZEREGOWANY.ELEMENT.MODUŁU +CUBESET = ZESTAW.MODUŁÓW +CUBESETCOUNT = LICZNIK.MODUŁÓW.ZESTAWU +CUBEVALUE = WARTOŚĆ.MODUŁU + +## +## Funkcje baz danych (Database Functions) +## +DAVERAGE = BD.ŚREDNIA +DCOUNT = BD.ILE.REKORDÓW +DCOUNTA = BD.ILE.REKORDÓW.A +DGET = BD.POLE +DMAX = BD.MAX +DMIN = BD.MIN +DPRODUCT = BD.ILOCZYN +DSTDEV = BD.ODCH.STANDARD +DSTDEVP = BD.ODCH.STANDARD.POPUL +DSUM = BD.SUMA +DVAR = BD.WARIANCJA +DVARP = BD.WARIANCJA.POPUL + +## +## Funkcje daty i godziny (Date & Time Functions) +## +DATE = DATA +DATEDIF = DATA.RÓŻNICA +DATESTRING = DATA.CIĄG.ZNAK +DATEVALUE = DATA.WARTOŚĆ +DAY = DZIEŃ +DAYS = DNI +DAYS360 = DNI.360 +EDATE = NR.SER.DATY +EOMONTH = NR.SER.OST.DN.MIES +HOUR = GODZINA +ISOWEEKNUM = ISO.NUM.TYG +MINUTE = MINUTA +MONTH = MIESIĄC +NETWORKDAYS = DNI.ROBOCZE +NETWORKDAYS.INTL = DNI.ROBOCZE.NIESTAND +NOW = TERAZ +SECOND = SEKUNDA +THAIDAYOFWEEK = TAJ.DZIEŃ.TYGODNIA +THAIMONTHOFYEAR = TAJ.MIESIĄC.ROKU +THAIYEAR = TAJ.ROK +TIME = CZAS +TIMEVALUE = CZAS.WARTOŚĆ +TODAY = DZIŚ +WEEKDAY = DZIEŃ.TYG +WEEKNUM = NUM.TYG +WORKDAY = DZIEŃ.ROBOCZY +WORKDAY.INTL = DZIEŃ.ROBOCZY.NIESTAND +YEAR = ROK +YEARFRAC = CZĘŚĆ.ROKU + +## +## Funkcje inżynierskie (Engineering Functions) +## +BESSELI = BESSEL.I +BESSELJ = BESSEL.J +BESSELK = BESSEL.K +BESSELY = BESSEL.Y +BIN2DEC = DWÓJK.NA.DZIES +BIN2HEX = DWÓJK.NA.SZESN +BIN2OCT = DWÓJK.NA.ÓSM +BITAND = BITAND +BITLSHIFT = BIT.PRZESUNIĘCIE.W.LEWO +BITOR = BITOR +BITRSHIFT = BIT.PRZESUNIĘCIE.W.PRAWO +BITXOR = BITXOR +COMPLEX = LICZBA.ZESP +CONVERT = KONWERTUJ +DEC2BIN = DZIES.NA.DWÓJK +DEC2HEX = DZIES.NA.SZESN +DEC2OCT = DZIES.NA.ÓSM +DELTA = CZY.RÓWNE +ERF = FUNKCJA.BŁ +ERF.PRECISE = FUNKCJA.BŁ.DOKŁ +ERFC = KOMP.FUNKCJA.BŁ +ERFC.PRECISE = KOMP.FUNKCJA.BŁ.DOKŁ +GESTEP = SPRAWDŹ.PRÓG +HEX2BIN = SZESN.NA.DWÓJK +HEX2DEC = SZESN.NA.DZIES +HEX2OCT = SZESN.NA.ÓSM +IMABS = MODUŁ.LICZBY.ZESP +IMAGINARY = CZ.UROJ.LICZBY.ZESP +IMARGUMENT = ARG.LICZBY.ZESP +IMCONJUGATE = SPRZĘŻ.LICZBY.ZESP +IMCOS = COS.LICZBY.ZESP +IMCOSH = COSH.LICZBY.ZESP +IMCOT = COT.LICZBY.ZESP +IMCSC = CSC.LICZBY.ZESP +IMCSCH = CSCH.LICZBY.ZESP +IMDIV = ILORAZ.LICZB.ZESP +IMEXP = EXP.LICZBY.ZESP +IMLN = LN.LICZBY.ZESP +IMLOG10 = LOG10.LICZBY.ZESP +IMLOG2 = LOG2.LICZBY.ZESP +IMPOWER = POTĘGA.LICZBY.ZESP +IMPRODUCT = ILOCZYN.LICZB.ZESP +IMREAL = CZ.RZECZ.LICZBY.ZESP +IMSEC = SEC.LICZBY.ZESP +IMSECH = SECH.LICZBY.ZESP +IMSIN = SIN.LICZBY.ZESP +IMSINH = SINH.LICZBY.ZESP +IMSQRT = PIERWIASTEK.LICZBY.ZESP +IMSUB = RÓŻN.LICZB.ZESP +IMSUM = SUMA.LICZB.ZESP +IMTAN = TAN.LICZBY.ZESP +OCT2BIN = ÓSM.NA.DWÓJK +OCT2DEC = ÓSM.NA.DZIES +OCT2HEX = ÓSM.NA.SZESN + +## +## Funkcje finansowe (Financial Functions) +## +ACCRINT = NAL.ODS +ACCRINTM = NAL.ODS.WYKUP +AMORDEGRC = AMORT.NIELIN +AMORLINC = AMORT.LIN +COUPDAYBS = WYPŁ.DNI.OD.POCZ +COUPDAYS = WYPŁ.DNI +COUPDAYSNC = WYPŁ.DNI.NAST +COUPNCD = WYPŁ.DATA.NAST +COUPNUM = WYPŁ.LICZBA +COUPPCD = WYPŁ.DATA.POPRZ +CUMIPMT = SPŁAC.ODS +CUMPRINC = SPŁAC.KAPIT +DB = DB +DDB = DDB +DISC = STOPA.DYSK +DOLLARDE = CENA.DZIES +DOLLARFR = CENA.UŁAM +DURATION = ROCZ.PRZYCH +EFFECT = EFEKTYWNA +FV = FV +FVSCHEDULE = WART.PRZYSZŁ.KAP +INTRATE = STOPA.PROC +IPMT = IPMT +IRR = IRR +ISPMT = ISPMT +MDURATION = ROCZ.PRZYCH.M +MIRR = MIRR +NOMINAL = NOMINALNA +NPER = NPER +NPV = NPV +ODDFPRICE = CENA.PIERW.OKR +ODDFYIELD = RENT.PIERW.OKR +ODDLPRICE = CENA.OST.OKR +ODDLYIELD = RENT.OST.OKR +PDURATION = O.CZAS.TRWANIA +PMT = PMT +PPMT = PPMT +PRICE = CENA +PRICEDISC = CENA.DYSK +PRICEMAT = CENA.WYKUP +PV = PV +RATE = RATE +RECEIVED = KWOTA.WYKUP +RRI = RÓWNOW.STOPA.PROC +SLN = SLN +SYD = SYD +TBILLEQ = RENT.EKW.BS +TBILLPRICE = CENA.BS +TBILLYIELD = RENT.BS +VDB = VDB +XIRR = XIRR +XNPV = XNPV +YIELD = RENTOWNOŚĆ +YIELDDISC = RENT.DYSK +YIELDMAT = RENT.WYKUP + +## +## Funkcje informacyjne (Information Functions) +## +CELL = KOMÓRKA +ERROR.TYPE = NR.BŁĘDU +INFO = INFO +ISBLANK = CZY.PUSTA +ISERR = CZY.BŁ +ISERROR = CZY.BŁĄD +ISEVEN = CZY.PARZYSTE +ISFORMULA = CZY.FORMUŁA +ISLOGICAL = CZY.LOGICZNA +ISNA = CZY.BRAK +ISNONTEXT = CZY.NIE.TEKST +ISNUMBER = CZY.LICZBA +ISODD = CZY.NIEPARZYSTE +ISREF = CZY.ADR +ISTEXT = CZY.TEKST +N = N +NA = BRAK +SHEET = ARKUSZ +SHEETS = ARKUSZE +TYPE = TYP + +## +## Funkcje logiczne (Logical Functions) +## +AND = ORAZ +FALSE = FAŁSZ +IF = JEŻELI +IFERROR = JEŻELI.BŁĄD +IFNA = JEŻELI.ND +IFS = WARUNKI +NOT = NIE +OR = LUB +SWITCH = PRZEŁĄCZ +TRUE = PRAWDA +XOR = XOR + +## +## Funkcje wyszukiwania i odwołań (Lookup & Reference Functions) +## +ADDRESS = ADRES +AREAS = OBSZARY +CHOOSE = WYBIERZ +COLUMN = NR.KOLUMNY +COLUMNS = LICZBA.KOLUMN +FORMULATEXT = FORMUŁA.TEKST +GETPIVOTDATA = WEŹDANETABELI +HLOOKUP = WYSZUKAJ.POZIOMO +HYPERLINK = HIPERŁĄCZE +INDEX = INDEKS +INDIRECT = ADR.POŚR +LOOKUP = WYSZUKAJ +MATCH = PODAJ.POZYCJĘ +OFFSET = PRZESUNIĘCIE +ROW = WIERSZ +ROWS = ILE.WIERSZY +RTD = DANE.CZASU.RZECZ +TRANSPOSE = TRANSPONUJ +VLOOKUP = WYSZUKAJ.PIONOWO + +## +## Funkcje matematyczne i trygonometryczne (Math & Trig Functions) +## +ABS = MODUŁ.LICZBY +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = AGREGUJ +ARABIC = ARABSKIE +ASIN = ASIN +ASINH = ASINH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = PODSTAWA +CEILING.MATH = ZAOKR.W.GÓRĘ.MATEMATYCZNE +CEILING.PRECISE = ZAOKR.W.GÓRĘ.DOKŁ +COMBIN = KOMBINACJE +COMBINA = KOMBINACJE.A +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DZIESIĘTNA +DEGREES = STOPNIE +ECMA.CEILING = ECMA.ZAOKR.W.GÓRĘ +EVEN = ZAOKR.DO.PARZ +EXP = EXP +FACT = SILNIA +FACTDOUBLE = SILNIA.DWUKR +FLOOR.MATH = ZAOKR.W.DÓŁ.MATEMATYCZNE +FLOOR.PRECISE = ZAOKR.W.DÓŁ.DOKŁ +GCD = NAJW.WSP.DZIEL +INT = ZAOKR.DO.CAŁK +ISO.CEILING = ISO.ZAOKR.W.GÓRĘ +LCM = NAJMN.WSP.WIEL +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = WYZNACZNIK.MACIERZY +MINVERSE = MACIERZ.ODW +MMULT = MACIERZ.ILOCZYN +MOD = MOD +MROUND = ZAOKR.DO.WIELOKR +MULTINOMIAL = WIELOMIAN +MUNIT = MACIERZ.JEDNOSTKOWA +ODD = ZAOKR.DO.NPARZ +PI = PI +POWER = POTĘGA +PRODUCT = ILOCZYN +QUOTIENT = CZ.CAŁK.DZIELENIA +RADIANS = RADIANY +RAND = LOS +RANDBETWEEN = LOS.ZAKR +ROMAN = RZYMSKIE +ROUND = ZAOKR +ROUNDBAHTDOWN = ZAOKR.DÓŁ.BAT +ROUNDBAHTUP = ZAOKR.GÓRA.BAT +ROUNDDOWN = ZAOKR.DÓŁ +ROUNDUP = ZAOKR.GÓRA +SEC = SEC +SECH = SECH +SERIESSUM = SUMA.SZER.POT +SIGN = ZNAK.LICZBY +SIN = SIN +SINH = SINH +SQRT = PIERWIASTEK +SQRTPI = PIERW.PI +SUBTOTAL = SUMY.CZĘŚCIOWE +SUM = SUMA +SUMIF = SUMA.JEŻELI +SUMIFS = SUMA.WARUNKÓW +SUMPRODUCT = SUMA.ILOCZYNÓW +SUMSQ = SUMA.KWADRATÓW +SUMX2MY2 = SUMA.X2.M.Y2 +SUMX2PY2 = SUMA.X2.P.Y2 +SUMXMY2 = SUMA.XMY.2 +TAN = TAN +TANH = TANH +TRUNC = LICZBA.CAŁK + +## +## Funkcje statystyczne (Statistical Functions) +## +AVEDEV = ODCH.ŚREDNIE +AVERAGE = ŚREDNIA +AVERAGEA = ŚREDNIA.A +AVERAGEIF = ŚREDNIA.JEŻELI +AVERAGEIFS = ŚREDNIA.WARUNKÓW +BETA.DIST = ROZKŁ.BETA +BETA.INV = ROZKŁ.BETA.ODWR +BINOM.DIST = ROZKŁ.DWUM +BINOM.DIST.RANGE = ROZKŁ.DWUM.ZAKRES +BINOM.INV = ROZKŁ.DWUM.ODWR +CHISQ.DIST = ROZKŁ.CHI +CHISQ.DIST.RT = ROZKŁ.CHI.PS +CHISQ.INV = ROZKŁ.CHI.ODWR +CHISQ.INV.RT = ROZKŁ.CHI.ODWR.PS +CHISQ.TEST = CHI.TEST +CONFIDENCE.NORM = UFNOŚĆ.NORM +CONFIDENCE.T = UFNOŚĆ.T +CORREL = WSP.KORELACJI +COUNT = ILE.LICZB +COUNTA = ILE.NIEPUSTYCH +COUNTBLANK = LICZ.PUSTE +COUNTIF = LICZ.JEŻELI +COUNTIFS = LICZ.WARUNKI +COVARIANCE.P = KOWARIANCJA.POPUL +COVARIANCE.S = KOWARIANCJA.PRÓBKI +DEVSQ = ODCH.KWADRATOWE +EXPON.DIST = ROZKŁ.EXP +F.DIST = ROZKŁ.F +F.DIST.RT = ROZKŁ.F.PS +F.INV = ROZKŁ.F.ODWR +F.INV.RT = ROZKŁ.F.ODWR.PS +F.TEST = F.TEST +FISHER = ROZKŁAD.FISHER +FISHERINV = ROZKŁAD.FISHER.ODW +FORECAST.ETS = REGLINX.ETS +FORECAST.ETS.CONFINT = REGLINX.ETS.CONFINT +FORECAST.ETS.SEASONALITY = REGLINX.ETS.SEZONOWOŚĆ +FORECAST.ETS.STAT = REGLINX.ETS.STATYSTYKA +FORECAST.LINEAR = REGLINX.LINIOWA +FREQUENCY = CZĘSTOŚĆ +GAMMA = GAMMA +GAMMA.DIST = ROZKŁ.GAMMA +GAMMA.INV = ROZKŁ.GAMMA.ODWR +GAMMALN = ROZKŁAD.LIN.GAMMA +GAMMALN.PRECISE = ROZKŁAD.LIN.GAMMA.DOKŁ +GAUSS = GAUSS +GEOMEAN = ŚREDNIA.GEOMETRYCZNA +GROWTH = REGEXPW +HARMEAN = ŚREDNIA.HARMONICZNA +HYPGEOM.DIST = ROZKŁ.HIPERGEOM +INTERCEPT = ODCIĘTA +KURT = KURTOZA +LARGE = MAX.K +LINEST = REGLINP +LOGEST = REGEXPP +LOGNORM.DIST = ROZKŁ.LOG +LOGNORM.INV = ROZKŁ.LOG.ODWR +MAX = MAX +MAXA = MAX.A +MAXIFS = MAKS.WARUNKÓW +MEDIAN = MEDIANA +MIN = MIN +MINA = MIN.A +MINIFS = MIN.WARUNKÓW +MODE.MULT = WYST.NAJCZĘŚCIEJ.TABL +MODE.SNGL = WYST.NAJCZĘŚCIEJ.WART +NEGBINOM.DIST = ROZKŁ.DWUM.PRZEC +NORM.DIST = ROZKŁ.NORMALNY +NORM.INV = ROZKŁ.NORMALNY.ODWR +NORM.S.DIST = ROZKŁ.NORMALNY.S +NORM.S.INV = ROZKŁ.NORMALNY.S.ODWR +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTYL.PRZEDZ.OTW +PERCENTILE.INC = PERCENTYL.PRZEDZ.ZAMK +PERCENTRANK.EXC = PROC.POZ.PRZEDZ.OTW +PERCENTRANK.INC = PROC.POZ.PRZEDZ.ZAMK +PERMUT = PERMUTACJE +PERMUTATIONA = PERMUTACJE.A +PHI = PHI +POISSON.DIST = ROZKŁ.POISSON +PROB = PRAWDPD +QUARTILE.EXC = KWARTYL.PRZEDZ.OTW +QUARTILE.INC = KWARTYL.PRZEDZ.ZAMK +RANK.AVG = POZYCJA.ŚR +RANK.EQ = POZYCJA.NAJW +RSQ = R.KWADRAT +SKEW = SKOŚNOŚĆ +SKEW.P = SKOŚNOŚĆ.P +SLOPE = NACHYLENIE +SMALL = MIN.K +STANDARDIZE = NORMALIZUJ +STDEV.P = ODCH.STAND.POPUL +STDEV.S = ODCH.STANDARD.PRÓBKI +STDEVA = ODCH.STANDARDOWE.A +STDEVPA = ODCH.STANDARD.POPUL.A +STEYX = REGBŁSTD +T.DIST = ROZKŁ.T +T.DIST.2T = ROZKŁ.T.DS +T.DIST.RT = ROZKŁ.T.PS +T.INV = ROZKŁ.T.ODWR +T.INV.2T = ROZKŁ.T.ODWR.DS +T.TEST = T.TEST +TREND = REGLINW +TRIMMEAN = ŚREDNIA.WEWN +VAR.P = WARIANCJA.POP +VAR.S = WARIANCJA.PRÓBKI +VARA = WARIANCJA.A +VARPA = WARIANCJA.POPUL.A +WEIBULL.DIST = ROZKŁ.WEIBULL +Z.TEST = Z.TEST + +## +## Funkcje tekstowe (Text Functions) +## +BAHTTEXT = BAT.TEKST +CHAR = ZNAK +CLEAN = OCZYŚĆ +CODE = KOD +CONCAT = ZŁĄCZ.TEKST +DOLLAR = KWOTA +EXACT = PORÓWNAJ +FIND = ZNAJDŹ +FIXED = ZAOKR.DO.TEKST +ISTHAIDIGIT = CZY.CYFRA.TAJ +LEFT = LEWY +LEN = DŁ +LOWER = LITERY.MAŁE +MID = FRAGMENT.TEKSTU +NUMBERSTRING = LICZBA.CIĄG.ZNAK +NUMBERVALUE = WARTOŚĆ.LICZBOWA +PROPER = Z.WIELKIEJ.LITERY +REPLACE = ZASTĄP +REPT = POWT +RIGHT = PRAWY +SEARCH = SZUKAJ.TEKST +SUBSTITUTE = PODSTAW +T = T +TEXT = TEKST +TEXTJOIN = POŁĄCZ.TEKSTY +THAIDIGIT = TAJ.CYFRA +THAINUMSOUND = TAJ.DŹWIĘK.NUM +THAINUMSTRING = TAJ.CIĄG.NUM +THAISTRINGLENGTH = TAJ.DŁUGOŚĆ.CIĄGU +TRIM = USUŃ.ZBĘDNE.ODSTĘPY +UNICHAR = ZNAK.UNICODE +UNICODE = UNICODE +UPPER = LITERY.WIELKIE +VALUE = WARTOŚĆ + +## +## Funkcje sieci Web (Web Functions) +## +ENCODEURL = ENCODEURL +FILTERXML = FILTERXML +WEBSERVICE = WEBSERVICE + +## +## Funkcje zgodności (Compatibility Functions) +## +BETADIST = ROZKŁAD.BETA +BETAINV = ROZKŁAD.BETA.ODW +BINOMDIST = ROZKŁAD.DWUM +CEILING = ZAOKR.W.GÓRĘ +CHIDIST = ROZKŁAD.CHI +CHIINV = ROZKŁAD.CHI.ODW +CHITEST = TEST.CHI +CONCATENATE = ZŁĄCZ.TEKSTY +CONFIDENCE = UFNOŚĆ +COVAR = KOWARIANCJA +CRITBINOM = PRÓG.ROZKŁAD.DWUM +EXPONDIST = ROZKŁAD.EXP +FDIST = ROZKŁAD.F +FINV = ROZKŁAD.F.ODW +FLOOR = ZAOKR.W.DÓŁ +FORECAST = REGLINX +FTEST = TEST.F +GAMMADIST = ROZKŁAD.GAMMA +GAMMAINV = ROZKŁAD.GAMMA.ODW +HYPGEOMDIST = ROZKŁAD.HIPERGEOM +LOGINV = ROZKŁAD.LOG.ODW +LOGNORMDIST = ROZKŁAD.LOG +MODE = WYST.NAJCZĘŚCIEJ +NEGBINOMDIST = ROZKŁAD.DWUM.PRZEC +NORMDIST = ROZKŁAD.NORMALNY +NORMINV = ROZKŁAD.NORMALNY.ODW +NORMSDIST = ROZKŁAD.NORMALNY.S +NORMSINV = ROZKŁAD.NORMALNY.S.ODW +PERCENTILE = PERCENTYL +PERCENTRANK = PROCENT.POZYCJA +POISSON = ROZKŁAD.POISSON +QUARTILE = KWARTYL +RANK = POZYCJA +STDEV = ODCH.STANDARDOWE +STDEVP = ODCH.STANDARD.POPUL +TDIST = ROZKŁAD.T +TINV = ROZKŁAD.T.ODW +TTEST = TEST.T +VAR = WARIANCJA +VARP = WARIANCJA.POPUL +WEIBULL = ROZKŁAD.WEIBULL +ZTEST = TEST.Z diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/br/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/br/config new file mode 100644 index 0000000..c39057c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/br/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Português Brasileiro (Brazilian Portuguese) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #NULO! +DIV0 +VALUE = #VALOR! +REF +NAME = #NOME? +NUM = #NÚM! +NA = #N/D diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/br/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/br/functions new file mode 100644 index 0000000..5781b0c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/br/functions @@ -0,0 +1,528 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Português Brasileiro (Brazilian Portuguese) +## +############################################################ + + +## +## Funções de cubo (Cube Functions) +## +CUBEKPIMEMBER = MEMBROKPICUBO +CUBEMEMBER = MEMBROCUBO +CUBEMEMBERPROPERTY = PROPRIEDADEMEMBROCUBO +CUBERANKEDMEMBER = MEMBROCLASSIFICADOCUBO +CUBESET = CONJUNTOCUBO +CUBESETCOUNT = CONTAGEMCONJUNTOCUBO +CUBEVALUE = VALORCUBO + +## +## Funções de banco de dados (Database Functions) +## +DAVERAGE = BDMÉDIA +DCOUNT = BDCONTAR +DCOUNTA = BDCONTARA +DGET = BDEXTRAIR +DMAX = BDMÁX +DMIN = BDMÍN +DPRODUCT = BDMULTIPL +DSTDEV = BDEST +DSTDEVP = BDDESVPA +DSUM = BDSOMA +DVAR = BDVAREST +DVARP = BDVARP + +## +## Funções de data e hora (Date & Time Functions) +## +DATE = DATA +DATEDIF = DATADIF +DATESTRING = DATA.SÉRIE +DATEVALUE = DATA.VALOR +DAY = DIA +DAYS = DIAS +DAYS360 = DIAS360 +EDATE = DATAM +EOMONTH = FIMMÊS +HOUR = HORA +ISOWEEKNUM = NÚMSEMANAISO +MINUTE = MINUTO +MONTH = MÊS +NETWORKDAYS = DIATRABALHOTOTAL +NETWORKDAYS.INTL = DIATRABALHOTOTAL.INTL +NOW = AGORA +SECOND = SEGUNDO +TIME = TEMPO +TIMEVALUE = VALOR.TEMPO +TODAY = HOJE +WEEKDAY = DIA.DA.SEMANA +WEEKNUM = NÚMSEMANA +WORKDAY = DIATRABALHO +WORKDAY.INTL = DIATRABALHO.INTL +YEAR = ANO +YEARFRAC = FRAÇÃOANO + +## +## Funções de engenharia (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BINADEC +BIN2HEX = BINAHEX +BIN2OCT = BINAOCT +BITAND = BITAND +BITLSHIFT = DESLOCESQBIT +BITOR = BITOR +BITRSHIFT = DESLOCDIRBIT +BITXOR = BITXOR +COMPLEX = COMPLEXO +CONVERT = CONVERTER +DEC2BIN = DECABIN +DEC2HEX = DECAHEX +DEC2OCT = DECAOCT +DELTA = DELTA +ERF = FUNERRO +ERF.PRECISE = FUNERRO.PRECISO +ERFC = FUNERROCOMPL +ERFC.PRECISE = FUNERROCOMPL.PRECISO +GESTEP = DEGRAU +HEX2BIN = HEXABIN +HEX2DEC = HEXADEC +HEX2OCT = HEXAOCT +IMABS = IMABS +IMAGINARY = IMAGINÁRIO +IMARGUMENT = IMARG +IMCONJUGATE = IMCONJ +IMCOS = IMCOS +IMCOSH = IMCOSH +IMCOT = IMCOT +IMCSC = IMCOSEC +IMCSCH = IMCOSECH +IMDIV = IMDIV +IMEXP = IMEXP +IMLN = IMLN +IMLOG10 = IMLOG10 +IMLOG2 = IMLOG2 +IMPOWER = IMPOT +IMPRODUCT = IMPROD +IMREAL = IMREAL +IMSEC = IMSEC +IMSECH = IMSECH +IMSIN = IMSENO +IMSINH = IMSENH +IMSQRT = IMRAIZ +IMSUB = IMSUBTR +IMSUM = IMSOMA +IMTAN = IMTAN +OCT2BIN = OCTABIN +OCT2DEC = OCTADEC +OCT2HEX = OCTAHEX + +## +## Funções financeiras (Financial Functions) +## +ACCRINT = JUROSACUM +ACCRINTM = JUROSACUMV +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = CUPDIASINLIQ +COUPDAYS = CUPDIAS +COUPDAYSNC = CUPDIASPRÓX +COUPNCD = CUPDATAPRÓX +COUPNUM = CUPNÚM +COUPPCD = CUPDATAANT +CUMIPMT = PGTOJURACUM +CUMPRINC = PGTOCAPACUM +DB = BD +DDB = BDD +DISC = DESC +DOLLARDE = MOEDADEC +DOLLARFR = MOEDAFRA +DURATION = DURAÇÃO +EFFECT = EFETIVA +FV = VF +FVSCHEDULE = VFPLANO +INTRATE = TAXAJUROS +IPMT = IPGTO +IRR = TIR +ISPMT = ÉPGTO +MDURATION = MDURAÇÃO +MIRR = MTIR +NOMINAL = NOMINAL +NPER = NPER +NPV = VPL +ODDFPRICE = PREÇOPRIMINC +ODDFYIELD = LUCROPRIMINC +ODDLPRICE = PREÇOÚLTINC +ODDLYIELD = LUCROÚLTINC +PDURATION = DURAÇÃOP +PMT = PGTO +PPMT = PPGTO +PRICE = PREÇO +PRICEDISC = PREÇODESC +PRICEMAT = PREÇOVENC +PV = VP +RATE = TAXA +RECEIVED = RECEBER +RRI = TAXAJURO +SLN = DPD +SYD = SDA +TBILLEQ = OTN +TBILLPRICE = OTNVALOR +TBILLYIELD = OTNLUCRO +VDB = BDV +XIRR = XTIR +XNPV = XVPL +YIELD = LUCRO +YIELDDISC = LUCRODESC +YIELDMAT = LUCROVENC + +## +## Funções de informação (Information Functions) +## +CELL = CÉL +ERROR.TYPE = TIPO.ERRO +INFO = INFORMAÇÃO +ISBLANK = ÉCÉL.VAZIA +ISERR = ÉERRO +ISERROR = ÉERROS +ISEVEN = ÉPAR +ISFORMULA = ÉFÓRMULA +ISLOGICAL = ÉLÓGICO +ISNA = É.NÃO.DISP +ISNONTEXT = É.NÃO.TEXTO +ISNUMBER = ÉNÚM +ISODD = ÉIMPAR +ISREF = ÉREF +ISTEXT = ÉTEXTO +N = N +NA = NÃO.DISP +SHEET = PLAN +SHEETS = PLANS +TYPE = TIPO + +## +## Funções lógicas (Logical Functions) +## +AND = E +FALSE = FALSO +IF = SE +IFERROR = SEERRO +IFNA = SENÃODISP +IFS = SES +NOT = NÃO +OR = OU +SWITCH = PARÂMETRO +TRUE = VERDADEIRO +XOR = XOR + +## +## Funções de pesquisa e referência (Lookup & Reference Functions) +## +ADDRESS = ENDEREÇO +AREAS = ÁREAS +CHOOSE = ESCOLHER +COLUMN = COL +COLUMNS = COLS +FORMULATEXT = FÓRMULATEXTO +GETPIVOTDATA = INFODADOSTABELADINÂMICA +HLOOKUP = PROCH +HYPERLINK = HIPERLINK +INDEX = ÍNDICE +INDIRECT = INDIRETO +LOOKUP = PROC +MATCH = CORRESP +OFFSET = DESLOC +ROW = LIN +ROWS = LINS +RTD = RTD +TRANSPOSE = TRANSPOR +VLOOKUP = PROCV +*RC = LC + +## +## Funções matemáticas e trigonométricas (Math & Trig Functions) +## +ABS = ABS +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = AGREGAR +ARABIC = ARÁBICO +ASIN = ASEN +ASINH = ASENH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = BASE +CEILING.MATH = TETO.MAT +CEILING.PRECISE = TETO.PRECISO +COMBIN = COMBIN +COMBINA = COMBINA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = COSEC +CSCH = COSECH +DECIMAL = DECIMAL +DEGREES = GRAUS +ECMA.CEILING = ECMA.TETO +EVEN = PAR +EXP = EXP +FACT = FATORIAL +FACTDOUBLE = FATDUPLO +FLOOR.MATH = ARREDMULTB.MAT +FLOOR.PRECISE = ARREDMULTB.PRECISO +GCD = MDC +INT = INT +ISO.CEILING = ISO.TETO +LCM = MMC +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MATRIZ.DETERM +MINVERSE = MATRIZ.INVERSO +MMULT = MATRIZ.MULT +MOD = MOD +MROUND = MARRED +MULTINOMIAL = MULTINOMIAL +MUNIT = MUNIT +ODD = ÍMPAR +PI = PI +POWER = POTÊNCIA +PRODUCT = MULT +QUOTIENT = QUOCIENTE +RADIANS = RADIANOS +RAND = ALEATÓRIO +RANDBETWEEN = ALEATÓRIOENTRE +ROMAN = ROMANO +ROUND = ARRED +ROUNDDOWN = ARREDONDAR.PARA.BAIXO +ROUNDUP = ARREDONDAR.PARA.CIMA +SEC = SEC +SECH = SECH +SERIESSUM = SOMASEQÜÊNCIA +SIGN = SINAL +SIN = SEN +SINH = SENH +SQRT = RAIZ +SQRTPI = RAIZPI +SUBTOTAL = SUBTOTAL +SUM = SOMA +SUMIF = SOMASE +SUMIFS = SOMASES +SUMPRODUCT = SOMARPRODUTO +SUMSQ = SOMAQUAD +SUMX2MY2 = SOMAX2DY2 +SUMX2PY2 = SOMAX2SY2 +SUMXMY2 = SOMAXMY2 +TAN = TAN +TANH = TANH +TRUNC = TRUNCAR + +## +## Funções estatísticas (Statistical Functions) +## +AVEDEV = DESV.MÉDIO +AVERAGE = MÉDIA +AVERAGEA = MÉDIAA +AVERAGEIF = MÉDIASE +AVERAGEIFS = MÉDIASES +BETA.DIST = DIST.BETA +BETA.INV = INV.BETA +BINOM.DIST = DISTR.BINOM +BINOM.DIST.RANGE = INTERV.DISTR.BINOM +BINOM.INV = INV.BINOM +CHISQ.DIST = DIST.QUIQUA +CHISQ.DIST.RT = DIST.QUIQUA.CD +CHISQ.INV = INV.QUIQUA +CHISQ.INV.RT = INV.QUIQUA.CD +CHISQ.TEST = TESTE.QUIQUA +CONFIDENCE.NORM = INT.CONFIANÇA.NORM +CONFIDENCE.T = INT.CONFIANÇA.T +CORREL = CORREL +COUNT = CONT.NÚM +COUNTA = CONT.VALORES +COUNTBLANK = CONTAR.VAZIO +COUNTIF = CONT.SE +COUNTIFS = CONT.SES +COVARIANCE.P = COVARIAÇÃO.P +COVARIANCE.S = COVARIAÇÃO.S +DEVSQ = DESVQ +EXPON.DIST = DISTR.EXPON +F.DIST = DIST.F +F.DIST.RT = DIST.F.CD +F.INV = INV.F +F.INV.RT = INV.F.CD +F.TEST = TESTE.F +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = PREVISÃO.ETS +FORECAST.ETS.CONFINT = PREVISÃO.ETS.CONFINT +FORECAST.ETS.SEASONALITY = PREVISÃO.ETS.SAZONALIDADE +FORECAST.ETS.STAT = PREVISÃO.ETS.STAT +FORECAST.LINEAR = PREVISÃO.LINEAR +FREQUENCY = FREQÜÊNCIA +GAMMA = GAMA +GAMMA.DIST = DIST.GAMA +GAMMA.INV = INV.GAMA +GAMMALN = LNGAMA +GAMMALN.PRECISE = LNGAMA.PRECISO +GAUSS = GAUSS +GEOMEAN = MÉDIA.GEOMÉTRICA +GROWTH = CRESCIMENTO +HARMEAN = MÉDIA.HARMÔNICA +HYPGEOM.DIST = DIST.HIPERGEOM.N +INTERCEPT = INTERCEPÇÃO +KURT = CURT +LARGE = MAIOR +LINEST = PROJ.LIN +LOGEST = PROJ.LOG +LOGNORM.DIST = DIST.LOGNORMAL.N +LOGNORM.INV = INV.LOGNORMAL +MAX = MÁXIMO +MAXA = MÁXIMOA +MAXIFS = MÁXIMOSES +MEDIAN = MED +MIN = MÍNIMO +MINA = MÍNIMOA +MINIFS = MÍNIMOSES +MODE.MULT = MODO.MULT +MODE.SNGL = MODO.ÚNICO +NEGBINOM.DIST = DIST.BIN.NEG.N +NORM.DIST = DIST.NORM.N +NORM.INV = INV.NORM.N +NORM.S.DIST = DIST.NORMP.N +NORM.S.INV = INV.NORMP.N +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTIL.EXC +PERCENTILE.INC = PERCENTIL.INC +PERCENTRANK.EXC = ORDEM.PORCENTUAL.EXC +PERCENTRANK.INC = ORDEM.PORCENTUAL.INC +PERMUT = PERMUT +PERMUTATIONA = PERMUTAS +PHI = PHI +POISSON.DIST = DIST.POISSON +PROB = PROB +QUARTILE.EXC = QUARTIL.EXC +QUARTILE.INC = QUARTIL.INC +RANK.AVG = ORDEM.MÉD +RANK.EQ = ORDEM.EQ +RSQ = RQUAD +SKEW = DISTORÇÃO +SKEW.P = DISTORÇÃO.P +SLOPE = INCLINAÇÃO +SMALL = MENOR +STANDARDIZE = PADRONIZAR +STDEV.P = DESVPAD.P +STDEV.S = DESVPAD.A +STDEVA = DESVPADA +STDEVPA = DESVPADPA +STEYX = EPADYX +T.DIST = DIST.T +T.DIST.2T = DIST.T.BC +T.DIST.RT = DIST.T.CD +T.INV = INV.T +T.INV.2T = INV.T.BC +T.TEST = TESTE.T +TREND = TENDÊNCIA +TRIMMEAN = MÉDIA.INTERNA +VAR.P = VAR.P +VAR.S = VAR.A +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = DIST.WEIBULL +Z.TEST = TESTE.Z + +## +## Funções de texto (Text Functions) +## +BAHTTEXT = BAHTTEXT +CHAR = CARACT +CLEAN = TIRAR +CODE = CÓDIGO +CONCAT = CONCAT +DOLLAR = MOEDA +EXACT = EXATO +FIND = PROCURAR +FIXED = DEF.NÚM.DEC +LEFT = ESQUERDA +LEN = NÚM.CARACT +LOWER = MINÚSCULA +MID = EXT.TEXTO +NUMBERSTRING = SEQÜÊNCIA.NÚMERO +NUMBERVALUE = VALORNUMÉRICO +PHONETIC = FONÉTICA +PROPER = PRI.MAIÚSCULA +REPLACE = MUDAR +REPT = REPT +RIGHT = DIREITA +SEARCH = LOCALIZAR +SUBSTITUTE = SUBSTITUIR +T = T +TEXT = TEXTO +TEXTJOIN = UNIRTEXTO +TRIM = ARRUMAR +UNICHAR = CARACTUNICODE +UNICODE = UNICODE +UPPER = MAIÚSCULA +VALUE = VALOR + +## +## Funções da Web (Web Functions) +## +ENCODEURL = CODIFURL +FILTERXML = FILTROXML +WEBSERVICE = SERVIÇOWEB + +## +## Funções de compatibilidade (Compatibility Functions) +## +BETADIST = DISTBETA +BETAINV = BETA.ACUM.INV +BINOMDIST = DISTRBINOM +CEILING = TETO +CHIDIST = DIST.QUI +CHIINV = INV.QUI +CHITEST = TESTE.QUI +CONCATENATE = CONCATENAR +CONFIDENCE = INT.CONFIANÇA +COVAR = COVAR +CRITBINOM = CRIT.BINOM +EXPONDIST = DISTEXPON +FDIST = DISTF +FINV = INVF +FLOOR = ARREDMULTB +FORECAST = PREVISÃO +FTEST = TESTEF +GAMMADIST = DISTGAMA +GAMMAINV = INVGAMA +HYPGEOMDIST = DIST.HIPERGEOM +LOGINV = INVLOG +LOGNORMDIST = DIST.LOGNORMAL +MODE = MODO +NEGBINOMDIST = DIST.BIN.NEG +NORMDIST = DISTNORM +NORMINV = INV.NORM +NORMSDIST = DISTNORMP +NORMSINV = INV.NORMP +PERCENTILE = PERCENTIL +PERCENTRANK = ORDEM.PORCENTUAL +POISSON = POISSON +QUARTILE = QUARTIL +RANK = ORDEM +STDEV = DESVPAD +STDEVP = DESVPADP +TDIST = DISTT +TINV = INVT +TTEST = TESTET +VAR = VAR +VARP = VARP +WEIBULL = WEIBULL +ZTEST = TESTEZ diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/config new file mode 100644 index 0000000..e661830 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Português (Portuguese) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #NULO! +DIV0 +VALUE = #VALOR! +REF +NAME = #NOME? +NUM = #NÚM! +NA = #N/D diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/functions new file mode 100644 index 0000000..70a3bb0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/pt/functions @@ -0,0 +1,538 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Português (Portuguese) +## +############################################################ + + +## +## Funções de cubo (Cube Functions) +## +CUBEKPIMEMBER = MEMBROKPICUBO +CUBEMEMBER = MEMBROCUBO +CUBEMEMBERPROPERTY = PROPRIEDADEMEMBROCUBO +CUBERANKEDMEMBER = MEMBROCLASSIFICADOCUBO +CUBESET = CONJUNTOCUBO +CUBESETCOUNT = CONTARCONJUNTOCUBO +CUBEVALUE = VALORCUBO + +## +## Funções de base de dados (Database Functions) +## +DAVERAGE = BDMÉDIA +DCOUNT = BDCONTAR +DCOUNTA = BDCONTAR.VAL +DGET = BDOBTER +DMAX = BDMÁX +DMIN = BDMÍN +DPRODUCT = BDMULTIPL +DSTDEV = BDDESVPAD +DSTDEVP = BDDESVPADP +DSUM = BDSOMA +DVAR = BDVAR +DVARP = BDVARP + +## +## Funções de data e hora (Date & Time Functions) +## +DATE = DATA +DATEDIF = DATADIF +DATESTRING = DATA.CADEIA +DATEVALUE = DATA.VALOR +DAY = DIA +DAYS = DIAS +DAYS360 = DIAS360 +EDATE = DATAM +EOMONTH = FIMMÊS +HOUR = HORA +ISOWEEKNUM = NUMSEMANAISO +MINUTE = MINUTO +MONTH = MÊS +NETWORKDAYS = DIATRABALHOTOTAL +NETWORKDAYS.INTL = DIATRABALHOTOTAL.INTL +NOW = AGORA +SECOND = SEGUNDO +THAIDAYOFWEEK = DIA.DA.SEMANA.TAILANDÊS +THAIMONTHOFYEAR = MÊS.DO.ANO.TAILANDÊS +THAIYEAR = ANO.TAILANDÊS +TIME = TEMPO +TIMEVALUE = VALOR.TEMPO +TODAY = HOJE +WEEKDAY = DIA.SEMANA +WEEKNUM = NÚMSEMANA +WORKDAY = DIATRABALHO +WORKDAY.INTL = DIATRABALHO.INTL +YEAR = ANO +YEARFRAC = FRAÇÃOANO + +## +## Funções de engenharia (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BINADEC +BIN2HEX = BINAHEX +BIN2OCT = BINAOCT +BITAND = BIT.E +BITLSHIFT = BITDESL.ESQ +BITOR = BIT.OU +BITRSHIFT = BITDESL.DIR +BITXOR = BIT.XOU +COMPLEX = COMPLEXO +CONVERT = CONVERTER +DEC2BIN = DECABIN +DEC2HEX = DECAHEX +DEC2OCT = DECAOCT +DELTA = DELTA +ERF = FUNCERRO +ERF.PRECISE = FUNCERRO.PRECISO +ERFC = FUNCERROCOMPL +ERFC.PRECISE = FUNCERROCOMPL.PRECISO +GESTEP = DEGRAU +HEX2BIN = HEXABIN +HEX2DEC = HEXADEC +HEX2OCT = HEXAOCT +IMABS = IMABS +IMAGINARY = IMAGINÁRIO +IMARGUMENT = IMARG +IMCONJUGATE = IMCONJ +IMCOS = IMCOS +IMCOSH = IMCOSH +IMCOT = IMCOT +IMCSC = IMCSC +IMCSCH = IMCSCH +IMDIV = IMDIV +IMEXP = IMEXP +IMLN = IMLN +IMLOG10 = IMLOG10 +IMLOG2 = IMLOG2 +IMPOWER = IMPOT +IMPRODUCT = IMPROD +IMREAL = IMREAL +IMSEC = IMSEC +IMSECH = IMSECH +IMSIN = IMSENO +IMSINH = IMSENOH +IMSQRT = IMRAIZ +IMSUB = IMSUBTR +IMSUM = IMSOMA +IMTAN = IMTAN +OCT2BIN = OCTABIN +OCT2DEC = OCTADEC +OCT2HEX = OCTAHEX + +## +## Funções financeiras (Financial Functions) +## +ACCRINT = JUROSACUM +ACCRINTM = JUROSACUMV +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = CUPDIASINLIQ +COUPDAYS = CUPDIAS +COUPDAYSNC = CUPDIASPRÓX +COUPNCD = CUPDATAPRÓX +COUPNUM = CUPNÚM +COUPPCD = CUPDATAANT +CUMIPMT = PGTOJURACUM +CUMPRINC = PGTOCAPACUM +DB = BD +DDB = BDD +DISC = DESC +DOLLARDE = MOEDADEC +DOLLARFR = MOEDAFRA +DURATION = DURAÇÃO +EFFECT = EFETIVA +FV = VF +FVSCHEDULE = VFPLANO +INTRATE = TAXAJUROS +IPMT = IPGTO +IRR = TIR +ISPMT = É.PGTO +MDURATION = MDURAÇÃO +MIRR = MTIR +NOMINAL = NOMINAL +NPER = NPER +NPV = VAL +ODDFPRICE = PREÇOPRIMINC +ODDFYIELD = LUCROPRIMINC +ODDLPRICE = PREÇOÚLTINC +ODDLYIELD = LUCROÚLTINC +PDURATION = PDURAÇÃO +PMT = PGTO +PPMT = PPGTO +PRICE = PREÇO +PRICEDISC = PREÇODESC +PRICEMAT = PREÇOVENC +PV = VA +RATE = TAXA +RECEIVED = RECEBER +RRI = DEVOLVERTAXAJUROS +SLN = AMORT +SYD = AMORTD +TBILLEQ = OTN +TBILLPRICE = OTNVALOR +TBILLYIELD = OTNLUCRO +VDB = BDV +XIRR = XTIR +XNPV = XVAL +YIELD = LUCRO +YIELDDISC = LUCRODESC +YIELDMAT = LUCROVENC + +## +## Funções de informação (Information Functions) +## +CELL = CÉL +ERROR.TYPE = TIPO.ERRO +INFO = INFORMAÇÃO +ISBLANK = É.CÉL.VAZIA +ISERR = É.ERROS +ISERROR = É.ERRO +ISEVEN = ÉPAR +ISFORMULA = É.FORMULA +ISLOGICAL = É.LÓGICO +ISNA = É.NÃO.DISP +ISNONTEXT = É.NÃO.TEXTO +ISNUMBER = É.NÚM +ISODD = ÉÍMPAR +ISREF = É.REF +ISTEXT = É.TEXTO +N = N +NA = NÃO.DISP +SHEET = FOLHA +SHEETS = FOLHAS +TYPE = TIPO + +## +## Funções lógicas (Logical Functions) +## +AND = E +FALSE = FALSO +IF = SE +IFERROR = SE.ERRO +IFNA = SEND +IFS = SE.S +NOT = NÃO +OR = OU +SWITCH = PARÂMETRO +TRUE = VERDADEIRO +XOR = XOU + +## +## Funções de pesquisa e referência (Lookup & Reference Functions) +## +ADDRESS = ENDEREÇO +AREAS = ÁREAS +CHOOSE = SELECIONAR +COLUMN = COL +COLUMNS = COLS +FORMULATEXT = FÓRMULA.TEXTO +GETPIVOTDATA = OBTERDADOSDIN +HLOOKUP = PROCH +HYPERLINK = HIPERLIGAÇÃO +INDEX = ÍNDICE +INDIRECT = INDIRETO +LOOKUP = PROC +MATCH = CORRESP +OFFSET = DESLOCAMENTO +ROW = LIN +ROWS = LINS +RTD = RTD +TRANSPOSE = TRANSPOR +VLOOKUP = PROCV +*RC = LC + +## +## Funções matemáticas e trigonométricas (Math & Trig Functions) +## +ABS = ABS +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = AGREGAR +ARABIC = ÁRABE +ASIN = ASEN +ASINH = ASENH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = BASE +CEILING.MATH = ARRED.EXCESSO.MAT +CEILING.PRECISE = ARRED.EXCESSO.PRECISO +COMBIN = COMBIN +COMBINA = COMBIN.R +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DECIMAL +DEGREES = GRAUS +ECMA.CEILING = ARRED.EXCESSO.ECMA +EVEN = PAR +EXP = EXP +FACT = FATORIAL +FACTDOUBLE = FATDUPLO +FLOOR.MATH = ARRED.DEFEITO.MAT +FLOOR.PRECISE = ARRED.DEFEITO.PRECISO +GCD = MDC +INT = INT +ISO.CEILING = ARRED.EXCESSO.ISO +LCM = MMC +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MATRIZ.DETERM +MINVERSE = MATRIZ.INVERSA +MMULT = MATRIZ.MULT +MOD = RESTO +MROUND = MARRED +MULTINOMIAL = POLINOMIAL +MUNIT = UNIDM +ODD = ÍMPAR +PI = PI +POWER = POTÊNCIA +PRODUCT = PRODUTO +QUOTIENT = QUOCIENTE +RADIANS = RADIANOS +RAND = ALEATÓRIO +RANDBETWEEN = ALEATÓRIOENTRE +ROMAN = ROMANO +ROUND = ARRED +ROUNDBAHTDOWN = ARREDOND.BAHT.BAIXO +ROUNDBAHTUP = ARREDOND.BAHT.CIMA +ROUNDDOWN = ARRED.PARA.BAIXO +ROUNDUP = ARRED.PARA.CIMA +SEC = SEC +SECH = SECH +SERIESSUM = SOMASÉRIE +SIGN = SINAL +SIN = SEN +SINH = SENH +SQRT = RAIZQ +SQRTPI = RAIZPI +SUBTOTAL = SUBTOTAL +SUM = SOMA +SUMIF = SOMA.SE +SUMIFS = SOMA.SE.S +SUMPRODUCT = SOMARPRODUTO +SUMSQ = SOMARQUAD +SUMX2MY2 = SOMAX2DY2 +SUMX2PY2 = SOMAX2SY2 +SUMXMY2 = SOMAXMY2 +TAN = TAN +TANH = TANH +TRUNC = TRUNCAR + +## +## Funções estatísticas (Statistical Functions) +## +AVEDEV = DESV.MÉDIO +AVERAGE = MÉDIA +AVERAGEA = MÉDIAA +AVERAGEIF = MÉDIA.SE +AVERAGEIFS = MÉDIA.SE.S +BETA.DIST = DIST.BETA +BETA.INV = INV.BETA +BINOM.DIST = DISTR.BINOM +BINOM.DIST.RANGE = DIST.BINOM.INTERVALO +BINOM.INV = INV.BINOM +CHISQ.DIST = DIST.CHIQ +CHISQ.DIST.RT = DIST.CHIQ.DIR +CHISQ.INV = INV.CHIQ +CHISQ.INV.RT = INV.CHIQ.DIR +CHISQ.TEST = TESTE.CHIQ +CONFIDENCE.NORM = INT.CONFIANÇA.NORM +CONFIDENCE.T = INT.CONFIANÇA.T +CORREL = CORREL +COUNT = CONTAR +COUNTA = CONTAR.VAL +COUNTBLANK = CONTAR.VAZIO +COUNTIF = CONTAR.SE +COUNTIFS = CONTAR.SE.S +COVARIANCE.P = COVARIÂNCIA.P +COVARIANCE.S = COVARIÂNCIA.S +DEVSQ = DESVQ +EXPON.DIST = DIST.EXPON +F.DIST = DIST.F +F.DIST.RT = DIST.F.DIR +F.INV = INV.F +F.INV.RT = INV.F.DIR +F.TEST = TESTE.F +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = PREVISÃO.ETS +FORECAST.ETS.CONFINT = PREVISÃO.ETS.CONFINT +FORECAST.ETS.SEASONALITY = PREVISÃO.ETS.SAZONALIDADE +FORECAST.ETS.STAT = PREVISÃO.ETS.ESTATÍSTICA +FORECAST.LINEAR = PREVISÃO.LINEAR +FREQUENCY = FREQUÊNCIA +GAMMA = GAMA +GAMMA.DIST = DIST.GAMA +GAMMA.INV = INV.GAMA +GAMMALN = LNGAMA +GAMMALN.PRECISE = LNGAMA.PRECISO +GAUSS = GAUSS +GEOMEAN = MÉDIA.GEOMÉTRICA +GROWTH = CRESCIMENTO +HARMEAN = MÉDIA.HARMÓNICA +HYPGEOM.DIST = DIST.HIPGEOM +INTERCEPT = INTERCETAR +KURT = CURT +LARGE = MAIOR +LINEST = PROJ.LIN +LOGEST = PROJ.LOG +LOGNORM.DIST = DIST.NORMLOG +LOGNORM.INV = INV.NORMALLOG +MAX = MÁXIMO +MAXA = MÁXIMOA +MAXIFS = MÁXIMO.SE.S +MEDIAN = MED +MIN = MÍNIMO +MINA = MÍNIMOA +MINIFS = MÍNIMO.SE.S +MODE.MULT = MODO.MÚLT +MODE.SNGL = MODO.SIMPLES +NEGBINOM.DIST = DIST.BINOM.NEG +NORM.DIST = DIST.NORMAL +NORM.INV = INV.NORMAL +NORM.S.DIST = DIST.S.NORM +NORM.S.INV = INV.S.NORM +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTIL.EXC +PERCENTILE.INC = PERCENTIL.INC +PERCENTRANK.EXC = ORDEM.PERCENTUAL.EXC +PERCENTRANK.INC = ORDEM.PERCENTUAL.INC +PERMUT = PERMUTAR +PERMUTATIONA = PERMUTAR.R +PHI = PHI +POISSON.DIST = DIST.POISSON +PROB = PROB +QUARTILE.EXC = QUARTIL.EXC +QUARTILE.INC = QUARTIL.INC +RANK.AVG = ORDEM.MÉD +RANK.EQ = ORDEM.EQ +RSQ = RQUAD +SKEW = DISTORÇÃO +SKEW.P = DISTORÇÃO.P +SLOPE = DECLIVE +SMALL = MENOR +STANDARDIZE = NORMALIZAR +STDEV.P = DESVPAD.P +STDEV.S = DESVPAD.S +STDEVA = DESVPADA +STDEVPA = DESVPADPA +STEYX = EPADYX +T.DIST = DIST.T +T.DIST.2T = DIST.T.2C +T.DIST.RT = DIST.T.DIR +T.INV = INV.T +T.INV.2T = INV.T.2C +T.TEST = TESTE.T +TREND = TENDÊNCIA +TRIMMEAN = MÉDIA.INTERNA +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = DIST.WEIBULL +Z.TEST = TESTE.Z + +## +## Funções de texto (Text Functions) +## +BAHTTEXT = TEXTO.BAHT +CHAR = CARÁT +CLEAN = LIMPARB +CODE = CÓDIGO +CONCAT = CONCAT +DOLLAR = MOEDA +EXACT = EXATO +FIND = LOCALIZAR +FIXED = FIXA +ISTHAIDIGIT = É.DÍGITO.TAILANDÊS +LEFT = ESQUERDA +LEN = NÚM.CARAT +LOWER = MINÚSCULAS +MID = SEG.TEXTO +NUMBERSTRING = NÚMERO.CADEIA +NUMBERVALUE = VALOR.NÚMERO +PHONETIC = FONÉTICA +PROPER = INICIAL.MAIÚSCULA +REPLACE = SUBSTITUIR +REPT = REPETIR +RIGHT = DIREITA +SEARCH = PROCURAR +SUBSTITUTE = SUBST +T = T +TEXT = TEXTO +TEXTJOIN = UNIRTEXTO +THAIDIGIT = DÍGITO.TAILANDÊS +THAINUMSOUND = SOM.NÚM.TAILANDÊS +THAINUMSTRING = CADEIA.NÚM.TAILANDÊS +THAISTRINGLENGTH = COMP.CADEIA.TAILANDÊS +TRIM = COMPACTAR +UNICHAR = UNICARÁT +UNICODE = UNICODE +UPPER = MAIÚSCULAS +VALUE = VALOR + +## +## Funções da Web (Web Functions) +## +ENCODEURL = CODIFICAÇÃOURL +FILTERXML = FILTRARXML +WEBSERVICE = SERVIÇOWEB + +## +## Funções de compatibilidade (Compatibility Functions) +## +BETADIST = DISTBETA +BETAINV = BETA.ACUM.INV +BINOMDIST = DISTRBINOM +CEILING = ARRED.EXCESSO +CHIDIST = DIST.CHI +CHIINV = INV.CHI +CHITEST = TESTE.CHI +CONCATENATE = CONCATENAR +CONFIDENCE = INT.CONFIANÇA +COVAR = COVAR +CRITBINOM = CRIT.BINOM +EXPONDIST = DISTEXPON +FDIST = DISTF +FINV = INVF +FLOOR = ARRED.DEFEITO +FORECAST = PREVISÃO +FTEST = TESTEF +GAMMADIST = DISTGAMA +GAMMAINV = INVGAMA +HYPGEOMDIST = DIST.HIPERGEOM +LOGINV = INVLOG +LOGNORMDIST = DIST.NORMALLOG +MODE = MODA +NEGBINOMDIST = DIST.BIN.NEG +NORMDIST = DIST.NORM +NORMINV = INV.NORM +NORMSDIST = DIST.NORMP +NORMSINV = INV.NORMP +PERCENTILE = PERCENTIL +PERCENTRANK = ORDEM.PERCENTUAL +POISSON = POISSON +QUARTILE = QUARTIL +RANK = ORDEM +STDEV = DESVPAD +STDEVP = DESVPADP +TDIST = DISTT +TINV = INVT +TTEST = TESTET +VAR = VAR +VARP = VARP +WEIBULL = WEIBULL +ZTEST = TESTEZ diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/ru/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/ru/config new file mode 100644 index 0000000..2a5a0db --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/ru/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## русский язык (Russian) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #ПУСТО! +DIV0 = #ДЕЛ/0! +VALUE = #ЗНАЧ! +REF = #ССЫЛКА! +NAME = #ИМЯ? +NUM = #ЧИСЛО! +NA = #Н/Д diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/ru/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/ru/functions new file mode 100644 index 0000000..9f05d5a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/ru/functions @@ -0,0 +1,555 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## русский язык (Russian) +## +############################################################ + + +## +## Функции кубов (Cube Functions) +## +CUBEKPIMEMBER = КУБЭЛЕМЕНТКИП +CUBEMEMBER = КУБЭЛЕМЕНТ +CUBEMEMBERPROPERTY = КУБСВОЙСТВОЭЛЕМЕНТА +CUBERANKEDMEMBER = КУБПОРЭЛЕМЕНТ +CUBESET = КУБМНОЖ +CUBESETCOUNT = КУБЧИСЛОЭЛМНОЖ +CUBEVALUE = КУБЗНАЧЕНИЕ + +## +## Функции для работы с базами данных (Database Functions) +## +DAVERAGE = ДСРЗНАЧ +DCOUNT = БСЧЁТ +DCOUNTA = БСЧЁТА +DGET = БИЗВЛЕЧЬ +DMAX = ДМАКС +DMIN = ДМИН +DPRODUCT = БДПРОИЗВЕД +DSTDEV = ДСТАНДОТКЛ +DSTDEVP = ДСТАНДОТКЛП +DSUM = БДСУММ +DVAR = БДДИСП +DVARP = БДДИСПП + +## +## Функции даты и времени (Date & Time Functions) +## +DATE = ДАТА +DATEDIF = РАЗНДАТ +DATESTRING = СТРОКАДАННЫХ +DATEVALUE = ДАТАЗНАЧ +DAY = ДЕНЬ +DAYS = ДНИ +DAYS360 = ДНЕЙ360 +EDATE = ДАТАМЕС +EOMONTH = КОНМЕСЯЦА +HOUR = ЧАС +ISOWEEKNUM = НОМНЕДЕЛИ.ISO +MINUTE = МИНУТЫ +MONTH = МЕСЯЦ +NETWORKDAYS = ЧИСТРАБДНИ +NETWORKDAYS.INTL = ЧИСТРАБДНИ.МЕЖД +NOW = ТДАТА +SECOND = СЕКУНДЫ +THAIDAYOFWEEK = ТАЙДЕНЬНЕД +THAIMONTHOFYEAR = ТАЙМЕСЯЦ +THAIYEAR = ТАЙГОД +TIME = ВРЕМЯ +TIMEVALUE = ВРЕМЗНАЧ +TODAY = СЕГОДНЯ +WEEKDAY = ДЕНЬНЕД +WEEKNUM = НОМНЕДЕЛИ +WORKDAY = РАБДЕНЬ +WORKDAY.INTL = РАБДЕНЬ.МЕЖД +YEAR = ГОД +YEARFRAC = ДОЛЯГОДА + +## +## Инженерные функции (Engineering Functions) +## +BESSELI = БЕССЕЛЬ.I +BESSELJ = БЕССЕЛЬ.J +BESSELK = БЕССЕЛЬ.K +BESSELY = БЕССЕЛЬ.Y +BIN2DEC = ДВ.В.ДЕС +BIN2HEX = ДВ.В.ШЕСТН +BIN2OCT = ДВ.В.ВОСЬМ +BITAND = БИТ.И +BITLSHIFT = БИТ.СДВИГЛ +BITOR = БИТ.ИЛИ +BITRSHIFT = БИТ.СДВИГП +BITXOR = БИТ.ИСКЛИЛИ +COMPLEX = КОМПЛЕКСН +CONVERT = ПРЕОБР +DEC2BIN = ДЕС.В.ДВ +DEC2HEX = ДЕС.В.ШЕСТН +DEC2OCT = ДЕС.В.ВОСЬМ +DELTA = ДЕЛЬТА +ERF = ФОШ +ERF.PRECISE = ФОШ.ТОЧН +ERFC = ДФОШ +ERFC.PRECISE = ДФОШ.ТОЧН +GESTEP = ПОРОГ +HEX2BIN = ШЕСТН.В.ДВ +HEX2DEC = ШЕСТН.В.ДЕС +HEX2OCT = ШЕСТН.В.ВОСЬМ +IMABS = МНИМ.ABS +IMAGINARY = МНИМ.ЧАСТЬ +IMARGUMENT = МНИМ.АРГУМЕНТ +IMCONJUGATE = МНИМ.СОПРЯЖ +IMCOS = МНИМ.COS +IMCOSH = МНИМ.COSH +IMCOT = МНИМ.COT +IMCSC = МНИМ.CSC +IMCSCH = МНИМ.CSCH +IMDIV = МНИМ.ДЕЛ +IMEXP = МНИМ.EXP +IMLN = МНИМ.LN +IMLOG10 = МНИМ.LOG10 +IMLOG2 = МНИМ.LOG2 +IMPOWER = МНИМ.СТЕПЕНЬ +IMPRODUCT = МНИМ.ПРОИЗВЕД +IMREAL = МНИМ.ВЕЩ +IMSEC = МНИМ.SEC +IMSECH = МНИМ.SECH +IMSIN = МНИМ.SIN +IMSINH = МНИМ.SINH +IMSQRT = МНИМ.КОРЕНЬ +IMSUB = МНИМ.РАЗН +IMSUM = МНИМ.СУММ +IMTAN = МНИМ.TAN +OCT2BIN = ВОСЬМ.В.ДВ +OCT2DEC = ВОСЬМ.В.ДЕС +OCT2HEX = ВОСЬМ.В.ШЕСТН + +## +## Финансовые функции (Financial Functions) +## +ACCRINT = НАКОПДОХОД +ACCRINTM = НАКОПДОХОДПОГАШ +AMORDEGRC = АМОРУМ +AMORLINC = АМОРУВ +COUPDAYBS = ДНЕЙКУПОНДО +COUPDAYS = ДНЕЙКУПОН +COUPDAYSNC = ДНЕЙКУПОНПОСЛЕ +COUPNCD = ДАТАКУПОНПОСЛЕ +COUPNUM = ЧИСЛКУПОН +COUPPCD = ДАТАКУПОНДО +CUMIPMT = ОБЩПЛАТ +CUMPRINC = ОБЩДОХОД +DB = ФУО +DDB = ДДОБ +DISC = СКИДКА +DOLLARDE = РУБЛЬ.ДЕС +DOLLARFR = РУБЛЬ.ДРОБЬ +DURATION = ДЛИТ +EFFECT = ЭФФЕКТ +FV = БС +FVSCHEDULE = БЗРАСПИС +INTRATE = ИНОРМА +IPMT = ПРПЛТ +IRR = ВСД +ISPMT = ПРОЦПЛАТ +MDURATION = МДЛИТ +MIRR = МВСД +NOMINAL = НОМИНАЛ +NPER = КПЕР +NPV = ЧПС +ODDFPRICE = ЦЕНАПЕРВНЕРЕГ +ODDFYIELD = ДОХОДПЕРВНЕРЕГ +ODDLPRICE = ЦЕНАПОСЛНЕРЕГ +ODDLYIELD = ДОХОДПОСЛНЕРЕГ +PDURATION = ПДЛИТ +PMT = ПЛТ +PPMT = ОСПЛТ +PRICE = ЦЕНА +PRICEDISC = ЦЕНАСКИДКА +PRICEMAT = ЦЕНАПОГАШ +PV = ПС +RATE = СТАВКА +RECEIVED = ПОЛУЧЕНО +RRI = ЭКВ.СТАВКА +SLN = АПЛ +SYD = АСЧ +TBILLEQ = РАВНОКЧЕК +TBILLPRICE = ЦЕНАКЧЕК +TBILLYIELD = ДОХОДКЧЕК +USDOLLAR = ДОЛЛСША +VDB = ПУО +XIRR = ЧИСТВНДОХ +XNPV = ЧИСТНЗ +YIELD = ДОХОД +YIELDDISC = ДОХОДСКИДКА +YIELDMAT = ДОХОДПОГАШ + +## +## Информационные функции (Information Functions) +## +CELL = ЯЧЕЙКА +ERROR.TYPE = ТИП.ОШИБКИ +INFO = ИНФОРМ +ISBLANK = ЕПУСТО +ISERR = ЕОШ +ISERROR = ЕОШИБКА +ISEVEN = ЕЧЁТН +ISFORMULA = ЕФОРМУЛА +ISLOGICAL = ЕЛОГИЧ +ISNA = ЕНД +ISNONTEXT = ЕНЕТЕКСТ +ISNUMBER = ЕЧИСЛО +ISODD = ЕНЕЧЁТ +ISREF = ЕССЫЛКА +ISTEXT = ЕТЕКСТ +N = Ч +NA = НД +SHEET = ЛИСТ +SHEETS = ЛИСТЫ +TYPE = ТИП + +## +## Логические функции (Logical Functions) +## +AND = И +FALSE = ЛОЖЬ +IF = ЕСЛИ +IFERROR = ЕСЛИОШИБКА +IFNA = ЕСНД +IFS = УСЛОВИЯ +NOT = НЕ +OR = ИЛИ +SWITCH = ПЕРЕКЛЮЧ +TRUE = ИСТИНА +XOR = ИСКЛИЛИ + +## +## Функции ссылки и поиска (Lookup & Reference Functions) +## +ADDRESS = АДРЕС +AREAS = ОБЛАСТИ +CHOOSE = ВЫБОР +COLUMN = СТОЛБЕЦ +COLUMNS = ЧИСЛСТОЛБ +FILTER = ФИЛЬТР +FORMULATEXT = Ф.ТЕКСТ +GETPIVOTDATA = ПОЛУЧИТЬ.ДАННЫЕ.СВОДНОЙ.ТАБЛИЦЫ +HLOOKUP = ГПР +HYPERLINK = ГИПЕРССЫЛКА +INDEX = ИНДЕКС +INDIRECT = ДВССЫЛ +LOOKUP = ПРОСМОТР +MATCH = ПОИСКПОЗ +OFFSET = СМЕЩ +ROW = СТРОКА +ROWS = ЧСТРОК +RTD = ДРВ +SORT = СОРТ +SORTBY = СОРТПО +TRANSPOSE = ТРАНСП +UNIQUE = УНИК +VLOOKUP = ВПР +XLOOKUP = ПРОСМОТРX +XMATCH = ПОИСКПОЗX + +## +## Математические и тригонометрические функции (Math & Trig Functions) +## +ABS = ABS +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = АГРЕГАТ +ARABIC = АРАБСКОЕ +ASIN = ASIN +ASINH = ASINH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = ОСНОВАНИЕ +CEILING.MATH = ОКРВВЕРХ.МАТ +CEILING.PRECISE = ОКРВВЕРХ.ТОЧН +COMBIN = ЧИСЛКОМБ +COMBINA = ЧИСЛКОМБА +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = ДЕС +DEGREES = ГРАДУСЫ +ECMA.CEILING = ECMA.ОКРВВЕРХ +EVEN = ЧЁТН +EXP = EXP +FACT = ФАКТР +FACTDOUBLE = ДВФАКТР +FLOOR.MATH = ОКРВНИЗ.МАТ +FLOOR.PRECISE = ОКРВНИЗ.ТОЧН +GCD = НОД +INT = ЦЕЛОЕ +ISO.CEILING = ISO.ОКРВВЕРХ +LCM = НОК +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = МОПРЕД +MINVERSE = МОБР +MMULT = МУМНОЖ +MOD = ОСТАТ +MROUND = ОКРУГЛТ +MULTINOMIAL = МУЛЬТИНОМ +MUNIT = МЕДИН +ODD = НЕЧЁТ +PI = ПИ +POWER = СТЕПЕНЬ +PRODUCT = ПРОИЗВЕД +QUOTIENT = ЧАСТНОЕ +RADIANS = РАДИАНЫ +RAND = СЛЧИС +RANDARRAY = СЛУЧМАССИВ +RANDBETWEEN = СЛУЧМЕЖДУ +ROMAN = РИМСКОЕ +ROUND = ОКРУГЛ +ROUNDBAHTDOWN = ОКРУГЛБАТВНИЗ +ROUNDBAHTUP = ОКРУГЛБАТВВЕРХ +ROUNDDOWN = ОКРУГЛВНИЗ +ROUNDUP = ОКРУГЛВВЕРХ +SEC = SEC +SECH = SECH +SERIESSUM = РЯД.СУММ +SEQUENCE = ПОСЛЕДОВ +SIGN = ЗНАК +SIN = SIN +SINH = SINH +SQRT = КОРЕНЬ +SQRTPI = КОРЕНЬПИ +SUBTOTAL = ПРОМЕЖУТОЧНЫЕ.ИТОГИ +SUM = СУММ +SUMIF = СУММЕСЛИ +SUMIFS = СУММЕСЛИМН +SUMPRODUCT = СУММПРОИЗВ +SUMSQ = СУММКВ +SUMX2MY2 = СУММРАЗНКВ +SUMX2PY2 = СУММСУММКВ +SUMXMY2 = СУММКВРАЗН +TAN = TAN +TANH = TANH +TRUNC = ОТБР + +## +## Статистические функции (Statistical Functions) +## +AVEDEV = СРОТКЛ +AVERAGE = СРЗНАЧ +AVERAGEA = СРЗНАЧА +AVERAGEIF = СРЗНАЧЕСЛИ +AVERAGEIFS = СРЗНАЧЕСЛИМН +BETA.DIST = БЕТА.РАСП +BETA.INV = БЕТА.ОБР +BINOM.DIST = БИНОМ.РАСП +BINOM.DIST.RANGE = БИНОМ.РАСП.ДИАП +BINOM.INV = БИНОМ.ОБР +CHISQ.DIST = ХИ2.РАСП +CHISQ.DIST.RT = ХИ2.РАСП.ПХ +CHISQ.INV = ХИ2.ОБР +CHISQ.INV.RT = ХИ2.ОБР.ПХ +CHISQ.TEST = ХИ2.ТЕСТ +CONFIDENCE.NORM = ДОВЕРИТ.НОРМ +CONFIDENCE.T = ДОВЕРИТ.СТЬЮДЕНТ +CORREL = КОРРЕЛ +COUNT = СЧЁТ +COUNTA = СЧЁТЗ +COUNTBLANK = СЧИТАТЬПУСТОТЫ +COUNTIF = СЧЁТЕСЛИ +COUNTIFS = СЧЁТЕСЛИМН +COVARIANCE.P = КОВАРИАЦИЯ.Г +COVARIANCE.S = КОВАРИАЦИЯ.В +DEVSQ = КВАДРОТКЛ +EXPON.DIST = ЭКСП.РАСП +F.DIST = F.РАСП +F.DIST.RT = F.РАСП.ПХ +F.INV = F.ОБР +F.INV.RT = F.ОБР.ПХ +F.TEST = F.ТЕСТ +FISHER = ФИШЕР +FISHERINV = ФИШЕРОБР +FORECAST.ETS = ПРЕДСКАЗ.ETS +FORECAST.ETS.CONFINT = ПРЕДСКАЗ.ЕTS.ДОВИНТЕРВАЛ +FORECAST.ETS.SEASONALITY = ПРЕДСКАЗ.ETS.СЕЗОННОСТЬ +FORECAST.ETS.STAT = ПРЕДСКАЗ.ETS.СТАТ +FORECAST.LINEAR = ПРЕДСКАЗ.ЛИНЕЙН +FREQUENCY = ЧАСТОТА +GAMMA = ГАММА +GAMMA.DIST = ГАММА.РАСП +GAMMA.INV = ГАММА.ОБР +GAMMALN = ГАММАНЛОГ +GAMMALN.PRECISE = ГАММАНЛОГ.ТОЧН +GAUSS = ГАУСС +GEOMEAN = СРГЕОМ +GROWTH = РОСТ +HARMEAN = СРГАРМ +HYPGEOM.DIST = ГИПЕРГЕОМ.РАСП +INTERCEPT = ОТРЕЗОК +KURT = ЭКСЦЕСС +LARGE = НАИБОЛЬШИЙ +LINEST = ЛИНЕЙН +LOGEST = ЛГРФПРИБЛ +LOGNORM.DIST = ЛОГНОРМ.РАСП +LOGNORM.INV = ЛОГНОРМ.ОБР +MAX = МАКС +MAXA = МАКСА +MAXIFS = МАКСЕСЛИ +MEDIAN = МЕДИАНА +MIN = МИН +MINA = МИНА +MINIFS = МИНЕСЛИ +MODE.MULT = МОДА.НСК +MODE.SNGL = МОДА.ОДН +NEGBINOM.DIST = ОТРБИНОМ.РАСП +NORM.DIST = НОРМ.РАСП +NORM.INV = НОРМ.ОБР +NORM.S.DIST = НОРМ.СТ.РАСП +NORM.S.INV = НОРМ.СТ.ОБР +PEARSON = PEARSON +PERCENTILE.EXC = ПРОЦЕНТИЛЬ.ИСКЛ +PERCENTILE.INC = ПРОЦЕНТИЛЬ.ВКЛ +PERCENTRANK.EXC = ПРОЦЕНТРАНГ.ИСКЛ +PERCENTRANK.INC = ПРОЦЕНТРАНГ.ВКЛ +PERMUT = ПЕРЕСТ +PERMUTATIONA = ПЕРЕСТА +PHI = ФИ +POISSON.DIST = ПУАССОН.РАСП +PROB = ВЕРОЯТНОСТЬ +QUARTILE.EXC = КВАРТИЛЬ.ИСКЛ +QUARTILE.INC = КВАРТИЛЬ.ВКЛ +RANK.AVG = РАНГ.СР +RANK.EQ = РАНГ.РВ +RSQ = КВПИРСОН +SKEW = СКОС +SKEW.P = СКОС.Г +SLOPE = НАКЛОН +SMALL = НАИМЕНЬШИЙ +STANDARDIZE = НОРМАЛИЗАЦИЯ +STDEV.P = СТАНДОТКЛОН.Г +STDEV.S = СТАНДОТКЛОН.В +STDEVA = СТАНДОТКЛОНА +STDEVPA = СТАНДОТКЛОНПА +STEYX = СТОШYX +T.DIST = СТЬЮДЕНТ.РАСП +T.DIST.2T = СТЬЮДЕНТ.РАСП.2Х +T.DIST.RT = СТЬЮДЕНТ.РАСП.ПХ +T.INV = СТЬЮДЕНТ.ОБР +T.INV.2T = СТЬЮДЕНТ.ОБР.2Х +T.TEST = СТЬЮДЕНТ.ТЕСТ +TREND = ТЕНДЕНЦИЯ +TRIMMEAN = УРЕЗСРЕДНЕЕ +VAR.P = ДИСП.Г +VAR.S = ДИСП.В +VARA = ДИСПА +VARPA = ДИСПРА +WEIBULL.DIST = ВЕЙБУЛЛ.РАСП +Z.TEST = Z.ТЕСТ + +## +## Текстовые функции (Text Functions) +## +ARRAYTOTEXT = МАССИВВТЕКСТ +BAHTTEXT = БАТТЕКСТ +CHAR = СИМВОЛ +CLEAN = ПЕЧСИМВ +CODE = КОДСИМВ +CONCAT = СЦЕП +DBCS = БДЦС +DOLLAR = РУБЛЬ +EXACT = СОВПАД +FIND = НАЙТИ +FINDB = НАЙТИБ +FIXED = ФИКСИРОВАННЫЙ +ISTHAIDIGIT = ЕТАЙЦИФРЫ +LEFT = ЛЕВСИМВ +LEFTB = ЛЕВБ +LEN = ДЛСТР +LENB = ДЛИНБ +LOWER = СТРОЧН +MID = ПСТР +MIDB = ПСТРБ +NUMBERSTRING = СТРОКАЧИСЕЛ +NUMBERVALUE = ЧЗНАЧ +PROPER = ПРОПНАЧ +REPLACE = ЗАМЕНИТЬ +REPLACEB = ЗАМЕНИТЬБ +REPT = ПОВТОР +RIGHT = ПРАВСИМВ +RIGHTB = ПРАВБ +SEARCH = ПОИСК +SEARCHB = ПОИСКБ +SUBSTITUTE = ПОДСТАВИТЬ +T = Т +TEXT = ТЕКСТ +TEXTJOIN = ОБЪЕДИНИТЬ +THAIDIGIT = ТАЙЦИФРА +THAINUMSOUND = ТАЙЧИСЛОВЗВУК +THAINUMSTRING = ТАЙЧИСЛОВСТРОКУ +THAISTRINGLENGTH = ТАЙДЛИНАСТРОКИ +TRIM = СЖПРОБЕЛЫ +UNICHAR = ЮНИСИМВ +UNICODE = UNICODE +UPPER = ПРОПИСН +VALUE = ЗНАЧЕН +VALUETOTEXT = ЗНАЧЕНИЕВТЕКСТ + +## +## Веб-функции (Web Functions) +## +ENCODEURL = КОДИР.URL +FILTERXML = ФИЛЬТР.XML +WEBSERVICE = ВЕБСЛУЖБА + +## +## Функции совместимости (Compatibility Functions) +## +BETADIST = БЕТАРАСП +BETAINV = БЕТАОБР +BINOMDIST = БИНОМРАСП +CEILING = ОКРВВЕРХ +CHIDIST = ХИ2РАСП +CHIINV = ХИ2ОБР +CHITEST = ХИ2ТЕСТ +CONCATENATE = СЦЕПИТЬ +CONFIDENCE = ДОВЕРИТ +COVAR = КОВАР +CRITBINOM = КРИТБИНОМ +EXPONDIST = ЭКСПРАСП +FDIST = FРАСП +FINV = FРАСПОБР +FLOOR = ОКРВНИЗ +FORECAST = ПРЕДСКАЗ +FTEST = ФТЕСТ +GAMMADIST = ГАММАРАСП +GAMMAINV = ГАММАОБР +HYPGEOMDIST = ГИПЕРГЕОМЕТ +LOGINV = ЛОГНОРМОБР +LOGNORMDIST = ЛОГНОРМРАСП +MODE = МОДА +NEGBINOMDIST = ОТРБИНОМРАСП +NORMDIST = НОРМРАСП +NORMINV = НОРМОБР +NORMSDIST = НОРМСТРАСП +NORMSINV = НОРМСТОБР +PERCENTILE = ПЕРСЕНТИЛЬ +PERCENTRANK = ПРОЦЕНТРАНГ +POISSON = ПУАССОН +QUARTILE = КВАРТИЛЬ +RANK = РАНГ +STDEV = СТАНДОТКЛОН +STDEVP = СТАНДОТКЛОНП +TDIST = СТЬЮДРАСП +TINV = СТЬЮДРАСПОБР +TTEST = ТТЕСТ +VAR = ДИСП +VARP = ДИСПР +WEIBULL = ВЕЙБУЛЛ +ZTEST = ZТЕСТ diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/sv/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/sv/config new file mode 100644 index 0000000..c7440f7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/sv/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Svenska (Swedish) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #SKÄRNING! +DIV0 = #DIVISION/0! +VALUE = #VÄRDEFEL! +REF = #REFERENS! +NAME = #NAMN? +NUM = #OGILTIGT! +NA = #SAKNAS! diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/sv/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/sv/functions new file mode 100644 index 0000000..491ecfb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/sv/functions @@ -0,0 +1,533 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Svenska (Swedish) +## +############################################################ + + +## +## Kubfunktioner (Cube Functions) +## +CUBEKPIMEMBER = KUBKPIMEDLEM +CUBEMEMBER = KUBMEDLEM +CUBEMEMBERPROPERTY = KUBMEDLEMSEGENSKAP +CUBERANKEDMEMBER = KUBRANGORDNADMEDLEM +CUBESET = KUBUPPSÄTTNING +CUBESETCOUNT = KUBUPPSÄTTNINGANTAL +CUBEVALUE = KUBVÄRDE + +## +## Databasfunktioner (Database Functions) +## +DAVERAGE = DMEDEL +DCOUNT = DANTAL +DCOUNTA = DANTALV +DGET = DHÄMTA +DMAX = DMAX +DMIN = DMIN +DPRODUCT = DPRODUKT +DSTDEV = DSTDAV +DSTDEVP = DSTDAVP +DSUM = DSUMMA +DVAR = DVARIANS +DVARP = DVARIANSP + +## +## Tid- och datumfunktioner (Date & Time Functions) +## +DATE = DATUM +DATEVALUE = DATUMVÄRDE +DAY = DAG +DAYS = DAGAR +DAYS360 = DAGAR360 +EDATE = EDATUM +EOMONTH = SLUTMÅNAD +HOUR = TIMME +ISOWEEKNUM = ISOVECKONR +MINUTE = MINUT +MONTH = MÅNAD +NETWORKDAYS = NETTOARBETSDAGAR +NETWORKDAYS.INTL = NETTOARBETSDAGAR.INT +NOW = NU +SECOND = SEKUND +THAIDAYOFWEEK = THAIVECKODAG +THAIMONTHOFYEAR = THAIMÅNAD +THAIYEAR = THAIÅR +TIME = KLOCKSLAG +TIMEVALUE = TIDVÄRDE +TODAY = IDAG +WEEKDAY = VECKODAG +WEEKNUM = VECKONR +WORKDAY = ARBETSDAGAR +WORKDAY.INTL = ARBETSDAGAR.INT +YEAR = ÅR +YEARFRAC = ÅRDEL + +## +## Tekniska funktioner (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BIN.TILL.DEC +BIN2HEX = BIN.TILL.HEX +BIN2OCT = BIN.TILL.OKT +BITAND = BITOCH +BITLSHIFT = BITVSKIFT +BITOR = BITELLER +BITRSHIFT = BITHSKIFT +BITXOR = BITXELLER +COMPLEX = KOMPLEX +CONVERT = KONVERTERA +DEC2BIN = DEC.TILL.BIN +DEC2HEX = DEC.TILL.HEX +DEC2OCT = DEC.TILL.OKT +DELTA = DELTA +ERF = FELF +ERF.PRECISE = FELF.EXAKT +ERFC = FELFK +ERFC.PRECISE = FELFK.EXAKT +GESTEP = SLSTEG +HEX2BIN = HEX.TILL.BIN +HEX2DEC = HEX.TILL.DEC +HEX2OCT = HEX.TILL.OKT +IMABS = IMABS +IMAGINARY = IMAGINÄR +IMARGUMENT = IMARGUMENT +IMCONJUGATE = IMKONJUGAT +IMCOS = IMCOS +IMCOSH = IMCOSH +IMCOT = IMCOT +IMCSC = IMCSC +IMCSCH = IMCSCH +IMDIV = IMDIV +IMEXP = IMEUPPHÖJT +IMLN = IMLN +IMLOG10 = IMLOG10 +IMLOG2 = IMLOG2 +IMPOWER = IMUPPHÖJT +IMPRODUCT = IMPRODUKT +IMREAL = IMREAL +IMSEC = IMSEK +IMSECH = IMSEKH +IMSIN = IMSIN +IMSINH = IMSINH +IMSQRT = IMROT +IMSUB = IMDIFF +IMSUM = IMSUM +IMTAN = IMTAN +OCT2BIN = OKT.TILL.BIN +OCT2DEC = OKT.TILL.DEC +OCT2HEX = OKT.TILL.HEX + +## +## Finansiella funktioner (Financial Functions) +## +ACCRINT = UPPLRÄNTA +ACCRINTM = UPPLOBLRÄNTA +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = KUPDAGBB +COUPDAYS = KUPDAGB +COUPDAYSNC = KUPDAGNK +COUPNCD = KUPNKD +COUPNUM = KUPANT +COUPPCD = KUPFKD +CUMIPMT = KUMRÄNTA +CUMPRINC = KUMPRIS +DB = DB +DDB = DEGAVSKR +DISC = DISK +DOLLARDE = DECTAL +DOLLARFR = BRÅK +DURATION = LÖPTID +EFFECT = EFFRÄNTA +FV = SLUTVÄRDE +FVSCHEDULE = FÖRRÄNTNING +INTRATE = ÅRSRÄNTA +IPMT = RBETALNING +IRR = IR +ISPMT = RALÅN +MDURATION = MLÖPTID +MIRR = MODIR +NOMINAL = NOMRÄNTA +NPER = PERIODER +NPV = NETNUVÄRDE +ODDFPRICE = UDDAFPRIS +ODDFYIELD = UDDAFAVKASTNING +ODDLPRICE = UDDASPRIS +ODDLYIELD = UDDASAVKASTNING +PDURATION = PLÖPTID +PMT = BETALNING +PPMT = AMORT +PRICE = PRIS +PRICEDISC = PRISDISK +PRICEMAT = PRISFÖRF +PV = NUVÄRDE +RATE = RÄNTA +RECEIVED = BELOPP +RRI = AVKPÅINVEST +SLN = LINAVSKR +SYD = ÅRSAVSKR +TBILLEQ = SSVXEKV +TBILLPRICE = SSVXPRIS +TBILLYIELD = SSVXRÄNTA +VDB = VDEGRAVSKR +XIRR = XIRR +XNPV = XNUVÄRDE +YIELD = NOMAVK +YIELDDISC = NOMAVKDISK +YIELDMAT = NOMAVKFÖRF + +## +## Informationsfunktioner (Information Functions) +## +CELL = CELL +ERROR.TYPE = FEL.TYP +INFO = INFO +ISBLANK = ÄRTOM +ISERR = ÄRF +ISERROR = ÄRFEL +ISEVEN = ÄRJÄMN +ISFORMULA = ÄRFORMEL +ISLOGICAL = ÄRLOGISK +ISNA = ÄRSAKNAD +ISNONTEXT = ÄREJTEXT +ISNUMBER = ÄRTAL +ISODD = ÄRUDDA +ISREF = ÄRREF +ISTEXT = ÄRTEXT +N = N +NA = SAKNAS +SHEET = BLAD +SHEETS = ANTALBLAD +TYPE = VÄRDETYP + +## +## Logiska funktioner (Logical Functions) +## +AND = OCH +FALSE = FALSKT +IF = OM +IFERROR = OMFEL +IFNA = OMSAKNAS +IFS = IFS +NOT = ICKE +OR = ELLER +SWITCH = VÄXLA +TRUE = SANT +XOR = XELLER + +## +## Sök- och referensfunktioner (Lookup & Reference Functions) +## +ADDRESS = ADRESS +AREAS = OMRÅDEN +CHOOSE = VÄLJ +COLUMN = KOLUMN +COLUMNS = KOLUMNER +FORMULATEXT = FORMELTEXT +GETPIVOTDATA = HÄMTA.PIVOTDATA +HLOOKUP = LETAKOLUMN +HYPERLINK = HYPERLÄNK +INDEX = INDEX +INDIRECT = INDIREKT +LOOKUP = LETAUPP +MATCH = PASSA +OFFSET = FÖRSKJUTNING +ROW = RAD +ROWS = RADER +RTD = RTD +TRANSPOSE = TRANSPONERA +VLOOKUP = LETARAD +*RC = RK + +## +## Matematiska och trigonometriska funktioner (Math & Trig Functions) +## +ABS = ABS +ACOS = ARCCOS +ACOSH = ARCCOSH +ACOT = ARCCOT +ACOTH = ARCCOTH +AGGREGATE = MÄNGD +ARABIC = ARABISKA +ASIN = ARCSIN +ASINH = ARCSINH +ATAN = ARCTAN +ATAN2 = ARCTAN2 +ATANH = ARCTANH +BASE = BAS +CEILING.MATH = RUNDA.UPP.MATEMATISKT +CEILING.PRECISE = RUNDA.UPP.EXAKT +COMBIN = KOMBIN +COMBINA = KOMBINA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = DECIMAL +DEGREES = GRADER +ECMA.CEILING = ECMA.RUNDA.UPP +EVEN = JÄMN +EXP = EXP +FACT = FAKULTET +FACTDOUBLE = DUBBELFAKULTET +FLOOR.MATH = RUNDA.NER.MATEMATISKT +FLOOR.PRECISE = RUNDA.NER.EXAKT +GCD = SGD +INT = HELTAL +ISO.CEILING = ISO.RUNDA.UPP +LCM = MGM +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = MDETERM +MINVERSE = MINVERT +MMULT = MMULT +MOD = REST +MROUND = MAVRUNDA +MULTINOMIAL = MULTINOMIAL +MUNIT = MENHET +ODD = UDDA +PI = PI +POWER = UPPHÖJT.TILL +PRODUCT = PRODUKT +QUOTIENT = KVOT +RADIANS = RADIANER +RAND = SLUMP +RANDBETWEEN = SLUMP.MELLAN +ROMAN = ROMERSK +ROUND = AVRUNDA +ROUNDBAHTDOWN = AVRUNDABAHTNEDÅT +ROUNDBAHTUP = AVRUNDABAHTUPPÅT +ROUNDDOWN = AVRUNDA.NEDÅT +ROUNDUP = AVRUNDA.UPPÅT +SEC = SEK +SECH = SEKH +SERIESSUM = SERIESUMMA +SIGN = TECKEN +SIN = SIN +SINH = SINH +SQRT = ROT +SQRTPI = ROTPI +SUBTOTAL = DELSUMMA +SUM = SUMMA +SUMIF = SUMMA.OM +SUMIFS = SUMMA.OMF +SUMPRODUCT = PRODUKTSUMMA +SUMSQ = KVADRATSUMMA +SUMX2MY2 = SUMMAX2MY2 +SUMX2PY2 = SUMMAX2PY2 +SUMXMY2 = SUMMAXMY2 +TAN = TAN +TANH = TANH +TRUNC = AVKORTA + +## +## Statistiska funktioner (Statistical Functions) +## +AVEDEV = MEDELAVV +AVERAGE = MEDEL +AVERAGEA = AVERAGEA +AVERAGEIF = MEDEL.OM +AVERAGEIFS = MEDEL.OMF +BETA.DIST = BETA.FÖRD +BETA.INV = BETA.INV +BINOM.DIST = BINOM.FÖRD +BINOM.DIST.RANGE = BINOM.FÖRD.INTERVALL +BINOM.INV = BINOM.INV +CHISQ.DIST = CHI2.FÖRD +CHISQ.DIST.RT = CHI2.FÖRD.RT +CHISQ.INV = CHI2.INV +CHISQ.INV.RT = CHI2.INV.RT +CHISQ.TEST = CHI2.TEST +CONFIDENCE.NORM = KONFIDENS.NORM +CONFIDENCE.T = KONFIDENS.T +CORREL = KORREL +COUNT = ANTAL +COUNTA = ANTALV +COUNTBLANK = ANTAL.TOMMA +COUNTIF = ANTAL.OM +COUNTIFS = ANTAL.OMF +COVARIANCE.P = KOVARIANS.P +COVARIANCE.S = KOVARIANS.S +DEVSQ = KVADAVV +EXPON.DIST = EXPON.FÖRD +F.DIST = F.FÖRD +F.DIST.RT = F.FÖRD.RT +F.INV = F.INV +F.INV.RT = F.INV.RT +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHERINV +FORECAST.ETS = PROGNOS.ETS +FORECAST.ETS.CONFINT = PROGNOS.ETS.KONFINT +FORECAST.ETS.SEASONALITY = PROGNOS.ETS.SÄSONGSBEROENDE +FORECAST.ETS.STAT = PROGNOS.ETS.STAT +FORECAST.LINEAR = PROGNOS.LINJÄR +FREQUENCY = FREKVENS +GAMMA = GAMMA +GAMMA.DIST = GAMMA.FÖRD +GAMMA.INV = GAMMA.INV +GAMMALN = GAMMALN +GAMMALN.PRECISE = GAMMALN.EXAKT +GAUSS = GAUSS +GEOMEAN = GEOMEDEL +GROWTH = EXPTREND +HARMEAN = HARMMEDEL +HYPGEOM.DIST = HYPGEOM.FÖRD +INTERCEPT = SKÄRNINGSPUNKT +KURT = TOPPIGHET +LARGE = STÖRSTA +LINEST = REGR +LOGEST = EXPREGR +LOGNORM.DIST = LOGNORM.FÖRD +LOGNORM.INV = LOGNORM.INV +MAX = MAX +MAXA = MAXA +MAXIFS = MAXIFS +MEDIAN = MEDIAN +MIN = MIN +MINA = MINA +MINIFS = MINIFS +MODE.MULT = TYPVÄRDE.FLERA +MODE.SNGL = TYPVÄRDE.ETT +NEGBINOM.DIST = NEGBINOM.FÖRD +NORM.DIST = NORM.FÖRD +NORM.INV = NORM.INV +NORM.S.DIST = NORM.S.FÖRD +NORM.S.INV = NORM.S.INV +PEARSON = PEARSON +PERCENTILE.EXC = PERCENTIL.EXK +PERCENTILE.INC = PERCENTIL.INK +PERCENTRANK.EXC = PROCENTRANG.EXK +PERCENTRANK.INC = PROCENTRANG.INK +PERMUT = PERMUT +PERMUTATIONA = PERMUTATIONA +PHI = PHI +POISSON.DIST = POISSON.FÖRD +PROB = SANNOLIKHET +QUARTILE.EXC = KVARTIL.EXK +QUARTILE.INC = KVARTIL.INK +RANK.AVG = RANG.MED +RANK.EQ = RANG.EKV +RSQ = RKV +SKEW = SNEDHET +SKEW.P = SNEDHET.P +SLOPE = LUTNING +SMALL = MINSTA +STANDARDIZE = STANDARDISERA +STDEV.P = STDAV.P +STDEV.S = STDAV.S +STDEVA = STDEVA +STDEVPA = STDEVPA +STEYX = STDFELYX +T.DIST = T.FÖRD +T.DIST.2T = T.FÖRD.2T +T.DIST.RT = T.FÖRD.RT +T.INV = T.INV +T.INV.2T = T.INV.2T +T.TEST = T.TEST +TREND = TREND +TRIMMEAN = TRIMMEDEL +VAR.P = VARIANS.P +VAR.S = VARIANS.S +VARA = VARA +VARPA = VARPA +WEIBULL.DIST = WEIBULL.FÖRD +Z.TEST = Z.TEST + +## +## Textfunktioner (Text Functions) +## +BAHTTEXT = BAHTTEXT +CHAR = TECKENKOD +CLEAN = STÄDA +CODE = KOD +CONCAT = SAMMAN +DOLLAR = VALUTA +EXACT = EXAKT +FIND = HITTA +FIXED = FASTTAL +LEFT = VÄNSTER +LEN = LÄNGD +LOWER = GEMENER +MID = EXTEXT +NUMBERVALUE = TALVÄRDE +PROPER = INITIAL +REPLACE = ERSÄTT +REPT = REP +RIGHT = HÖGER +SEARCH = SÖK +SUBSTITUTE = BYT.UT +T = T +TEXT = TEXT +TEXTJOIN = TEXTJOIN +THAIDIGIT = THAISIFFRA +THAINUMSOUND = THAITALLJUD +THAINUMSTRING = THAITALSTRÄNG +THAISTRINGLENGTH = THAISTRÄNGLÄNGD +TRIM = RENSA +UNICHAR = UNITECKENKOD +UNICODE = UNICODE +UPPER = VERSALER +VALUE = TEXTNUM + +## +## Webbfunktioner (Web Functions) +## +ENCODEURL = KODAWEBBADRESS +FILTERXML = FILTRERAXML +WEBSERVICE = WEBBTJÄNST + +## +## Kompatibilitetsfunktioner (Compatibility Functions) +## +BETADIST = BETAFÖRD +BETAINV = BETAINV +BINOMDIST = BINOMFÖRD +CEILING = RUNDA.UPP +CHIDIST = CHI2FÖRD +CHIINV = CHI2INV +CHITEST = CHI2TEST +CONCATENATE = SAMMANFOGA +CONFIDENCE = KONFIDENS +COVAR = KOVAR +CRITBINOM = KRITBINOM +EXPONDIST = EXPONFÖRD +FDIST = FFÖRD +FINV = FINV +FLOOR = RUNDA.NER +FORECAST = PREDIKTION +FTEST = FTEST +GAMMADIST = GAMMAFÖRD +GAMMAINV = GAMMAINV +HYPGEOMDIST = HYPGEOMFÖRD +LOGINV = LOGINV +LOGNORMDIST = LOGNORMFÖRD +MODE = TYPVÄRDE +NEGBINOMDIST = NEGBINOMFÖRD +NORMDIST = NORMFÖRD +NORMINV = NORMINV +NORMSDIST = NORMSFÖRD +NORMSINV = NORMSINV +PERCENTILE = PERCENTIL +PERCENTRANK = PROCENTRANG +POISSON = POISSON +QUARTILE = KVARTIL +RANK = RANG +STDEV = STDAV +STDEVP = STDAVP +TDIST = TFÖRD +TINV = TINV +TTEST = TTEST +VAR = VARIANS +VARP = VARIANSP +WEIBULL = WEIBULL +ZTEST = ZTEST diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/tr/config b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/tr/config new file mode 100644 index 0000000..63d22fd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/tr/config @@ -0,0 +1,20 @@ +############################################################ +## +## PhpSpreadsheet - locale settings +## +## Türkçe (Turkish) +## +############################################################ + +ArgumentSeparator = ; + +## +## Error Codes +## +NULL = #BOŞ! +DIV0 = #SAYI/0! +VALUE = #DEĞER! +REF = #BAŞV! +NAME = #AD? +NUM = #SAYI! +NA = #YOK diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/tr/functions b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/tr/functions new file mode 100644 index 0000000..f872274 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Calculation/locale/tr/functions @@ -0,0 +1,537 @@ +############################################################ +## +## PhpSpreadsheet - function name translations +## +## Türkçe (Turkish) +## +############################################################ + + +## +## Küp işlevleri (Cube Functions) +## +CUBEKPIMEMBER = KÜPKPIÜYESİ +CUBEMEMBER = KÜPÜYESİ +CUBEMEMBERPROPERTY = KÜPÜYEÖZELLİĞİ +CUBERANKEDMEMBER = DERECELİKÜPÜYESİ +CUBESET = KÜPKÜMESİ +CUBESETCOUNT = KÜPKÜMESAYISI +CUBEVALUE = KÜPDEĞERİ + +## +## Veritabanı işlevleri (Database Functions) +## +DAVERAGE = VSEÇORT +DCOUNT = VSEÇSAY +DCOUNTA = VSEÇSAYDOLU +DGET = VAL +DMAX = VSEÇMAK +DMIN = VSEÇMİN +DPRODUCT = VSEÇÇARP +DSTDEV = VSEÇSTDSAPMA +DSTDEVP = VSEÇSTDSAPMAS +DSUM = VSEÇTOPLA +DVAR = VSEÇVAR +DVARP = VSEÇVARS + +## +## Tarih ve saat işlevleri (Date & Time Functions) +## +DATE = TARİH +DATEDIF = ETARİHLİ +DATESTRING = TARİHDİZİ +DATEVALUE = TARİHSAYISI +DAY = GÜN +DAYS = GÜNSAY +DAYS360 = GÜN360 +EDATE = SERİTARİH +EOMONTH = SERİAY +HOUR = SAAT +ISOWEEKNUM = ISOHAFTASAY +MINUTE = DAKİKA +MONTH = AY +NETWORKDAYS = TAMİŞGÜNÜ +NETWORKDAYS.INTL = TAMİŞGÜNÜ.ULUSL +NOW = ŞİMDİ +SECOND = SANİYE +THAIDAYOFWEEK = TAYHAFTANINGÜNÜ +THAIMONTHOFYEAR = TAYYILINAYI +THAIYEAR = TAYYILI +TIME = ZAMAN +TIMEVALUE = ZAMANSAYISI +TODAY = BUGÜN +WEEKDAY = HAFTANINGÜNÜ +WEEKNUM = HAFTASAY +WORKDAY = İŞGÜNÜ +WORKDAY.INTL = İŞGÜNÜ.ULUSL +YEAR = YIL +YEARFRAC = YILORAN + +## +## Mühendislik işlevleri (Engineering Functions) +## +BESSELI = BESSELI +BESSELJ = BESSELJ +BESSELK = BESSELK +BESSELY = BESSELY +BIN2DEC = BIN2DEC +BIN2HEX = BIN2HEX +BIN2OCT = BIN2OCT +BITAND = BİTVE +BITLSHIFT = BİTSOLAKAYDIR +BITOR = BİTVEYA +BITRSHIFT = BİTSAĞAKAYDIR +BITXOR = BİTÖZELVEYA +COMPLEX = KARMAŞIK +CONVERT = ÇEVİR +DEC2BIN = DEC2BIN +DEC2HEX = DEC2HEX +DEC2OCT = DEC2OCT +DELTA = DELTA +ERF = HATAİŞLEV +ERF.PRECISE = HATAİŞLEV.DUYARLI +ERFC = TÜMHATAİŞLEV +ERFC.PRECISE = TÜMHATAİŞLEV.DUYARLI +GESTEP = BESINIR +HEX2BIN = HEX2BIN +HEX2DEC = HEX2DEC +HEX2OCT = HEX2OCT +IMABS = SANMUTLAK +IMAGINARY = SANAL +IMARGUMENT = SANBAĞ_DEĞİŞKEN +IMCONJUGATE = SANEŞLENEK +IMCOS = SANCOS +IMCOSH = SANCOSH +IMCOT = SANCOT +IMCSC = SANCSC +IMCSCH = SANCSCH +IMDIV = SANBÖL +IMEXP = SANÜS +IMLN = SANLN +IMLOG10 = SANLOG10 +IMLOG2 = SANLOG2 +IMPOWER = SANKUVVET +IMPRODUCT = SANÇARP +IMREAL = SANGERÇEK +IMSEC = SANSEC +IMSECH = SANSECH +IMSIN = SANSIN +IMSINH = SANSINH +IMSQRT = SANKAREKÖK +IMSUB = SANTOPLA +IMSUM = SANÇIKAR +IMTAN = SANTAN +OCT2BIN = OCT2BIN +OCT2DEC = OCT2DEC +OCT2HEX = OCT2HEX + +## +## Finansal işlevler (Financial Functions) +## +ACCRINT = GERÇEKFAİZ +ACCRINTM = GERÇEKFAİZV +AMORDEGRC = AMORDEGRC +AMORLINC = AMORLINC +COUPDAYBS = KUPONGÜNBD +COUPDAYS = KUPONGÜN +COUPDAYSNC = KUPONGÜNDSK +COUPNCD = KUPONGÜNSKT +COUPNUM = KUPONSAYI +COUPPCD = KUPONGÜNÖKT +CUMIPMT = TOPÖDENENFAİZ +CUMPRINC = TOPANAPARA +DB = AZALANBAKİYE +DDB = ÇİFTAZALANBAKİYE +DISC = İNDİRİM +DOLLARDE = LİRAON +DOLLARFR = LİRAKES +DURATION = SÜRE +EFFECT = ETKİN +FV = GD +FVSCHEDULE = GDPROGRAM +INTRATE = FAİZORANI +IPMT = FAİZTUTARI +IRR = İÇ_VERİM_ORANI +ISPMT = ISPMT +MDURATION = MSÜRE +MIRR = D_İÇ_VERİM_ORANI +NOMINAL = NOMİNAL +NPER = TAKSİT_SAYISI +NPV = NBD +ODDFPRICE = TEKYDEĞER +ODDFYIELD = TEKYÖDEME +ODDLPRICE = TEKSDEĞER +ODDLYIELD = TEKSÖDEME +PDURATION = PSÜRE +PMT = DEVRESEL_ÖDEME +PPMT = ANA_PARA_ÖDEMESİ +PRICE = DEĞER +PRICEDISC = DEĞERİND +PRICEMAT = DEĞERVADE +PV = BD +RATE = FAİZ_ORANI +RECEIVED = GETİRİ +RRI = GERÇEKLEŞENYATIRIMGETİRİSİ +SLN = DA +SYD = YAT +TBILLEQ = HTAHEŞ +TBILLPRICE = HTAHDEĞER +TBILLYIELD = HTAHÖDEME +VDB = DAB +XIRR = AİÇVERİMORANI +XNPV = ANBD +YIELD = ÖDEME +YIELDDISC = ÖDEMEİND +YIELDMAT = ÖDEMEVADE + +## +## Bilgi işlevleri (Information Functions) +## +CELL = HÜCRE +ERROR.TYPE = HATA.TİPİ +INFO = BİLGİ +ISBLANK = EBOŞSA +ISERR = EHATA +ISERROR = EHATALIYSA +ISEVEN = ÇİFTMİ +ISFORMULA = EFORMÜLSE +ISLOGICAL = EMANTIKSALSA +ISNA = EYOKSA +ISNONTEXT = EMETİNDEĞİLSE +ISNUMBER = ESAYIYSA +ISODD = TEKMİ +ISREF = EREFSE +ISTEXT = EMETİNSE +N = S +NA = YOKSAY +SHEET = SAYFA +SHEETS = SAYFALAR +TYPE = TÜR + +## +## Mantıksal işlevler (Logical Functions) +## +AND = VE +FALSE = YANLIŞ +IF = EĞER +IFERROR = EĞERHATA +IFNA = EĞERYOKSA +IFS = ÇOKEĞER +NOT = DEĞİL +OR = YADA +SWITCH = İLKEŞLEŞEN +TRUE = DOĞRU +XOR = ÖZELVEYA + +## +## Arama ve başvuru işlevleri (Lookup & Reference Functions) +## +ADDRESS = ADRES +AREAS = ALANSAY +CHOOSE = ELEMAN +COLUMN = SÜTUN +COLUMNS = SÜTUNSAY +FORMULATEXT = FORMÜLMETNİ +GETPIVOTDATA = ÖZETVERİAL +HLOOKUP = YATAYARA +HYPERLINK = KÖPRÜ +INDEX = İNDİS +INDIRECT = DOLAYLI +LOOKUP = ARA +MATCH = KAÇINCI +OFFSET = KAYDIR +ROW = SATIR +ROWS = SATIRSAY +RTD = GZV +TRANSPOSE = DEVRİK_DÖNÜŞÜM +VLOOKUP = DÜŞEYARA + +## +## Matematik ve trigonometri işlevleri (Math & Trig Functions) +## +ABS = MUTLAK +ACOS = ACOS +ACOSH = ACOSH +ACOT = ACOT +ACOTH = ACOTH +AGGREGATE = TOPLAMA +ARABIC = ARAP +ASIN = ASİN +ASINH = ASİNH +ATAN = ATAN +ATAN2 = ATAN2 +ATANH = ATANH +BASE = TABAN +CEILING.MATH = TAVANAYUVARLA.MATEMATİK +CEILING.PRECISE = TAVANAYUVARLA.DUYARLI +COMBIN = KOMBİNASYON +COMBINA = KOMBİNASYONA +COS = COS +COSH = COSH +COT = COT +COTH = COTH +CSC = CSC +CSCH = CSCH +DECIMAL = ONDALIK +DEGREES = DERECE +ECMA.CEILING = ECMA.TAVAN +EVEN = ÇİFT +EXP = ÜS +FACT = ÇARPINIM +FACTDOUBLE = ÇİFTFAKTÖR +FLOOR.MATH = TABANAYUVARLA.MATEMATİK +FLOOR.PRECISE = TABANAYUVARLA.DUYARLI +GCD = OBEB +INT = TAMSAYI +ISO.CEILING = ISO.TAVAN +LCM = OKEK +LN = LN +LOG = LOG +LOG10 = LOG10 +MDETERM = DETERMİNANT +MINVERSE = DİZEY_TERS +MMULT = DÇARP +MOD = MOD +MROUND = KYUVARLA +MULTINOMIAL = ÇOKTERİMLİ +MUNIT = BİRİMMATRİS +ODD = TEK +PI = Pİ +POWER = KUVVET +PRODUCT = ÇARPIM +QUOTIENT = BÖLÜM +RADIANS = RADYAN +RAND = S_SAYI_ÜRET +RANDBETWEEN = RASTGELEARADA +ROMAN = ROMEN +ROUND = YUVARLA +ROUNDBAHTDOWN = BAHTAŞAĞIYUVARLA +ROUNDBAHTUP = BAHTYUKARIYUVARLA +ROUNDDOWN = AŞAĞIYUVARLA +ROUNDUP = YUKARIYUVARLA +SEC = SEC +SECH = SECH +SERIESSUM = SERİTOPLA +SIGN = İŞARET +SIN = SİN +SINH = SİNH +SQRT = KAREKÖK +SQRTPI = KAREKÖKPİ +SUBTOTAL = ALTTOPLAM +SUM = TOPLA +SUMIF = ETOPLA +SUMIFS = ÇOKETOPLA +SUMPRODUCT = TOPLA.ÇARPIM +SUMSQ = TOPKARE +SUMX2MY2 = TOPX2EY2 +SUMX2PY2 = TOPX2AY2 +SUMXMY2 = TOPXEY2 +TAN = TAN +TANH = TANH +TRUNC = NSAT + +## +## İstatistik işlevleri (Statistical Functions) +## +AVEDEV = ORTSAP +AVERAGE = ORTALAMA +AVERAGEA = ORTALAMAA +AVERAGEIF = EĞERORTALAMA +AVERAGEIFS = ÇOKEĞERORTALAMA +BETA.DIST = BETA.DAĞ +BETA.INV = BETA.TERS +BINOM.DIST = BİNOM.DAĞ +BINOM.DIST.RANGE = BİNOM.DAĞ.ARALIK +BINOM.INV = BİNOM.TERS +CHISQ.DIST = KİKARE.DAĞ +CHISQ.DIST.RT = KİKARE.DAĞ.SAĞK +CHISQ.INV = KİKARE.TERS +CHISQ.INV.RT = KİKARE.TERS.SAĞK +CHISQ.TEST = KİKARE.TEST +CONFIDENCE.NORM = GÜVENİLİRLİK.NORM +CONFIDENCE.T = GÜVENİLİRLİK.T +CORREL = KORELASYON +COUNT = BAĞ_DEĞ_SAY +COUNTA = BAĞ_DEĞ_DOLU_SAY +COUNTBLANK = BOŞLUKSAY +COUNTIF = EĞERSAY +COUNTIFS = ÇOKEĞERSAY +COVARIANCE.P = KOVARYANS.P +COVARIANCE.S = KOVARYANS.S +DEVSQ = SAPKARE +EXPON.DIST = ÜSTEL.DAĞ +F.DIST = F.DAĞ +F.DIST.RT = F.DAĞ.SAĞK +F.INV = F.TERS +F.INV.RT = F.TERS.SAĞK +F.TEST = F.TEST +FISHER = FISHER +FISHERINV = FISHERTERS +FORECAST.ETS = TAHMİN.ETS +FORECAST.ETS.CONFINT = TAHMİN.ETS.GVNARAL +FORECAST.ETS.SEASONALITY = TAHMİN.ETS.MEVSİMSELLİK +FORECAST.ETS.STAT = TAHMİN.ETS.İSTAT +FORECAST.LINEAR = TAHMİN.DOĞRUSAL +FREQUENCY = SIKLIK +GAMMA = GAMA +GAMMA.DIST = GAMA.DAĞ +GAMMA.INV = GAMA.TERS +GAMMALN = GAMALN +GAMMALN.PRECISE = GAMALN.DUYARLI +GAUSS = GAUSS +GEOMEAN = GEOORT +GROWTH = BÜYÜME +HARMEAN = HARORT +HYPGEOM.DIST = HİPERGEOM.DAĞ +INTERCEPT = KESMENOKTASI +KURT = BASIKLIK +LARGE = BÜYÜK +LINEST = DOT +LOGEST = LOT +LOGNORM.DIST = LOGNORM.DAĞ +LOGNORM.INV = LOGNORM.TERS +MAX = MAK +MAXA = MAKA +MAXIFS = ÇOKEĞERMAK +MEDIAN = ORTANCA +MIN = MİN +MINA = MİNA +MINIFS = ÇOKEĞERMİN +MODE.MULT = ENÇOK_OLAN.ÇOK +MODE.SNGL = ENÇOK_OLAN.TEK +NEGBINOM.DIST = NEGBİNOM.DAĞ +NORM.DIST = NORM.DAĞ +NORM.INV = NORM.TERS +NORM.S.DIST = NORM.S.DAĞ +NORM.S.INV = NORM.S.TERS +PEARSON = PEARSON +PERCENTILE.EXC = YÜZDEBİRLİK.HRC +PERCENTILE.INC = YÜZDEBİRLİK.DHL +PERCENTRANK.EXC = YÜZDERANK.HRC +PERCENTRANK.INC = YÜZDERANK.DHL +PERMUT = PERMÜTASYON +PERMUTATIONA = PERMÜTASYONA +PHI = PHI +POISSON.DIST = POISSON.DAĞ +PROB = OLASILIK +QUARTILE.EXC = DÖRTTEBİRLİK.HRC +QUARTILE.INC = DÖRTTEBİRLİK.DHL +RANK.AVG = RANK.ORT +RANK.EQ = RANK.EŞİT +RSQ = RKARE +SKEW = ÇARPIKLIK +SKEW.P = ÇARPIKLIK.P +SLOPE = EĞİM +SMALL = KÜÇÜK +STANDARDIZE = STANDARTLAŞTIRMA +STDEV.P = STDSAPMA.P +STDEV.S = STDSAPMA.S +STDEVA = STDSAPMAA +STDEVPA = STDSAPMASA +STEYX = STHYX +T.DIST = T.DAĞ +T.DIST.2T = T.DAĞ.2K +T.DIST.RT = T.DAĞ.SAĞK +T.INV = T.TERS +T.INV.2T = T.TERS.2K +T.TEST = T.TEST +TREND = EĞİLİM +TRIMMEAN = KIRPORTALAMA +VAR.P = VAR.P +VAR.S = VAR.S +VARA = VARA +VARPA = VARSA +WEIBULL.DIST = WEIBULL.DAĞ +Z.TEST = Z.TEST + +## +## Metin işlevleri (Text Functions) +## +BAHTTEXT = BAHTMETİN +CHAR = DAMGA +CLEAN = TEMİZ +CODE = KOD +CONCAT = ARALIKBİRLEŞTİR +DOLLAR = LİRA +EXACT = ÖZDEŞ +FIND = BUL +FIXED = SAYIDÜZENLE +ISTHAIDIGIT = TAYRAKAMIYSA +LEFT = SOLDAN +LEN = UZUNLUK +LOWER = KÜÇÜKHARF +MID = PARÇAAL +NUMBERSTRING = SAYIDİZİ +NUMBERVALUE = SAYIDEĞERİ +PHONETIC = SES +PROPER = YAZIM.DÜZENİ +REPLACE = DEĞİŞTİR +REPT = YİNELE +RIGHT = SAĞDAN +SEARCH = MBUL +SUBSTITUTE = YERİNEKOY +T = M +TEXT = METNEÇEVİR +TEXTJOIN = METİNBİRLEŞTİR +THAIDIGIT = TAYRAKAM +THAINUMSOUND = TAYSAYISES +THAINUMSTRING = TAYSAYIDİZE +THAISTRINGLENGTH = TAYDİZEUZUNLUĞU +TRIM = KIRP +UNICHAR = UNICODEKARAKTERİ +UNICODE = UNICODE +UPPER = BÜYÜKHARF +VALUE = SAYIYAÇEVİR + +## +## Metin işlevleri (Web Functions) +## +ENCODEURL = URLKODLA +FILTERXML = XMLFİLTRELE +WEBSERVICE = WEBHİZMETİ + +## +## Uyumluluk işlevleri (Compatibility Functions) +## +BETADIST = BETADAĞ +BETAINV = BETATERS +BINOMDIST = BİNOMDAĞ +CEILING = TAVANAYUVARLA +CHIDIST = KİKAREDAĞ +CHIINV = KİKARETERS +CHITEST = KİKARETEST +CONCATENATE = BİRLEŞTİR +CONFIDENCE = GÜVENİRLİK +COVAR = KOVARYANS +CRITBINOM = KRİTİKBİNOM +EXPONDIST = ÜSTELDAĞ +FDIST = FDAĞ +FINV = FTERS +FLOOR = TABANAYUVARLA +FORECAST = TAHMİN +FTEST = FTEST +GAMMADIST = GAMADAĞ +GAMMAINV = GAMATERS +HYPGEOMDIST = HİPERGEOMDAĞ +LOGINV = LOGTERS +LOGNORMDIST = LOGNORMDAĞ +MODE = ENÇOK_OLAN +NEGBINOMDIST = NEGBİNOMDAĞ +NORMDIST = NORMDAĞ +NORMINV = NORMTERS +NORMSDIST = NORMSDAĞ +NORMSINV = NORMSTERS +PERCENTILE = YÜZDEBİRLİK +PERCENTRANK = YÜZDERANK +POISSON = POISSON +QUARTILE = DÖRTTEBİRLİK +RANK = RANK +STDEV = STDSAPMA +STDEVP = STDSAPMAS +TDIST = TDAĞ +TINV = TTERS +TTEST = TTEST +VAR = VAR +VARP = VARS +WEIBULL = WEIBULL +ZTEST = ZTEST diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/AddressHelper.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/AddressHelper.php new file mode 100644 index 0000000..9b48f59 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -0,0 +1,175 @@ +setValueExplicit(true, DataType::TYPE_BOOL); + + return true; + } elseif (StringHelper::strToUpper($value) === Calculation::getFALSE()) { + $cell->setValueExplicit(false, DataType::TYPE_BOOL); + + return true; + } + + // Check for fractions + if (preg_match('~^([+-]?)\s*(\d+)\s*/\s*(\d+)$~', $value, $matches)) { + return $this->setProperFraction($matches, $cell); + } elseif (preg_match('~^([+-]?)(\d+)\s+(\d+)\s*/\s*(\d+)$~', $value, $matches)) { + return $this->setImproperFraction($matches, $cell); + } + + $decimalSeparatorNoPreg = StringHelper::getDecimalSeparator(); + $decimalSeparator = preg_quote($decimalSeparatorNoPreg, '/'); + $thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/'); + + // Check for percentage + if (preg_match('/^\-?\d*' . $decimalSeparator . '?\d*\s?\%$/', (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value))) { + return $this->setPercentage((string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $cell); + } + + // Check for currency + if (preg_match(FormattedNumber::currencyMatcherRegexp(), (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $matches, PREG_UNMATCHED_AS_NULL)) { + // Convert value to number + $sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null; + $currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'] ?? ''; + /** @var string */ + $temp = str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value)); + $value = (float) ($sign . trim($temp)); + + return $this->setCurrency($value, $cell, $currencyCode); + } + + // Check for time without seconds e.g. '9:45', '09:45' + if (preg_match('/^(\d|[0-1]\d|2[0-3]):[0-5]\d$/', $value)) { + return $this->setTimeHoursMinutes($value, $cell); + } + + // Check for time with seconds '9:45:59', '09:45:59' + if (preg_match('/^(\d|[0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$/', $value)) { + return $this->setTimeHoursMinutesSeconds($value, $cell); + } + + // Check for datetime, e.g. '2008-12-31', '2008-12-31 15:59', '2008-12-31 15:59:10' + if (($d = Date::stringToExcel($value)) !== false) { + // Convert value to number + $cell->setValueExplicit($d, DataType::TYPE_NUMERIC); + // Determine style. Either there is a time part or not. Look for ':' + if (str_contains($value, ':')) { + $formatCode = 'yyyy-mm-dd h:mm'; + } else { + $formatCode = 'yyyy-mm-dd'; + } + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode($formatCode); + + return true; + } + + // Check for newline character "\n" + if (str_contains($value, "\n")) { + $cell->setValueExplicit($value, DataType::TYPE_STRING); + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getAlignment()->setWrapText(true); + + return true; + } + } + + // Not bound yet? Use parent... + return parent::bindValue($cell, $value); + } + + /** @param array{0: string, 1: ?string, 2: numeric-string, 3: numeric-string, 4: numeric-string} $matches */ + protected function setImproperFraction(array $matches, Cell $cell): bool + { + // Convert value to number + $value = $matches[2] + ($matches[3] / $matches[4]); + if ($matches[1] === '-') { + $value = 0 - $value; + } + $cell->setValueExplicit((float) $value, DataType::TYPE_NUMERIC); + + // Build the number format mask based on the size of the matched values + $dividend = str_repeat('?', strlen($matches[3])); + $divisor = str_repeat('?', strlen($matches[4])); + $fractionMask = "# {$dividend}/{$divisor}"; + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode($fractionMask); + + return true; + } + + /** @param array{0: string, 1: ?string, 2: numeric-string, 3: numeric-string} $matches */ + protected function setProperFraction(array $matches, Cell $cell): bool + { + // Convert value to number + $value = $matches[2] / $matches[3]; + if ($matches[1] === '-') { + $value = 0 - $value; + } + $cell->setValueExplicit((float) $value, DataType::TYPE_NUMERIC); + + // Build the number format mask based on the size of the matched values + $dividend = str_repeat('?', strlen($matches[2])); + $divisor = str_repeat('?', strlen($matches[3])); + $fractionMask = "{$dividend}/{$divisor}"; + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode($fractionMask); + + return true; + } + + protected function setPercentage(string $value, Cell $cell): bool + { + // Convert value to number + $value = ((float) str_replace('%', '', $value)) / 100; + $cell->setValueExplicit($value, DataType::TYPE_NUMERIC); + + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_PERCENTAGE_00); + + return true; + } + + protected function setCurrency(float $value, Cell $cell, string $currencyCode): bool + { + $cell->setValueExplicit($value, DataType::TYPE_NUMERIC); + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode( + str_replace('$', '[$' . $currencyCode . ']', NumberFormat::FORMAT_CURRENCY_USD) + ); + + return true; + } + + protected function setTimeHoursMinutes(string $value, Cell $cell): bool + { + // Convert value to number + [$hours, $minutes] = explode(':', $value); + $hours = (int) $hours; + $minutes = (int) $minutes; + $days = ($hours / 24) + ($minutes / 1440); + $cell->setValueExplicit($days, DataType::TYPE_NUMERIC); + + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_TIME3); + + return true; + } + + protected function setTimeHoursMinutesSeconds(string $value, Cell $cell): bool + { + // Convert value to number + [$hours, $minutes, $seconds] = explode(':', $value); + $hours = (int) $hours; + $minutes = (int) $minutes; + $seconds = (int) $seconds; + $days = ($hours / 24) + ($minutes / 1440) + ($seconds / 86400); + $cell->setValueExplicit($days, DataType::TYPE_NUMERIC); + + // Set style + $cell->getWorksheet()->getStyle($cell->getCoordinate()) + ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_TIME4); + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Cell.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Cell.php new file mode 100644 index 0000000..e2a84bb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Cell.php @@ -0,0 +1,1039 @@ + + */ + private ?array $formulaAttributes = null; + + private IgnoredErrors $ignoredErrors; + + /** + * Update the cell into the cell collection. + * + * @throws SpreadsheetException + */ + public function updateInCollection(): self + { + $parent = $this->parent; + if ($parent === null) { + throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet'); + } + $parent->update($this); + + return $this; + } + + public function detach(): void + { + $this->parent = null; + } + + public function attach(Cells $parent): void + { + $this->parent = $parent; + } + + /** + * Create a new Cell. + * + * @throws SpreadsheetException + */ + public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet) + { + // Initialise cell value + $this->value = $value; + + // Set worksheet cache + $this->parent = $worksheet->getCellCollection(); + + // Set datatype? + if ($dataType !== null) { + if ($dataType == DataType::TYPE_STRING2) { + $dataType = DataType::TYPE_STRING; + } + $this->dataType = $dataType; + } else { + $valueBinder = $worksheet->getParent()?->getValueBinder() ?? self::getValueBinder(); + if ($valueBinder->bindValue($this, $value) === false) { + throw new SpreadsheetException('Value could not be bound to cell.'); + } + } + $this->ignoredErrors = new IgnoredErrors(); + } + + /** + * Get cell coordinate column. + * + * @throws SpreadsheetException + */ + public function getColumn(): string + { + $parent = $this->parent; + if ($parent === null) { + throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet'); + } + + return $parent->getCurrentColumn(); + } + + /** + * Get cell coordinate row. + * + * @throws SpreadsheetException + */ + public function getRow(): int + { + $parent = $this->parent; + if ($parent === null) { + throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet'); + } + + return $parent->getCurrentRow(); + } + + /** + * Get cell coordinate. + * + * @throws SpreadsheetException + */ + public function getCoordinate(): string + { + $parent = $this->parent; + if ($parent !== null) { + $coordinate = $parent->getCurrentCoordinate(); + } else { + $coordinate = null; + } + if ($coordinate === null) { + throw new SpreadsheetException('Coordinate no longer exists'); + } + + return $coordinate; + } + + /** + * Get cell value. + */ + public function getValue(): mixed + { + return $this->value; + } + + public function getValueString(): string + { + return StringHelper::convertToString($this->value, false); + } + + /** + * Get cell value with formatting. + */ + public function getFormattedValue(): string + { + $currentCalendar = SharedDate::getExcelCalendar(); + SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar()); + $formattedValue = (string) NumberFormat::toFormattedString( + $this->getCalculatedValueString(), + (string) $this->getStyle()->getNumberFormat()->getFormatCode(true) + ); + SharedDate::setExcelCalendar($currentCalendar); + + return $formattedValue; + } + + protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void + { + $oldValue = StringHelper::convertToString($oldValue, false); + $newValue = StringHelper::convertToString($newValue, false); + if (StringHelper::strToLower($oldValue) === StringHelper::strToLower($newValue) || $workSheet === null) { + return; + } + + foreach ($workSheet->getTableCollection() as $table) { + /** @var Table $table */ + if ($cell->isInRange($table->getRange())) { + $rangeRowsColumns = Coordinate::getRangeBoundaries($table->getRange()); + if ($cell->getRow() === (int) $rangeRowsColumns[0][1]) { + Table\Column::updateStructuredReferences($workSheet, $oldValue, $newValue); + } + + return; + } + } + } + + /** + * Set cell value. + * + * Sets the value for a cell, automatically determining the datatype using the value binder + * + * @param mixed $value Value + * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder + * + * @throws SpreadsheetException + */ + public function setValue(mixed $value, ?IValueBinder $binder = null): self + { + // Cells?->Worksheet?->Spreadsheet + $binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder(); + if (!$binder->bindValue($this, $value)) { + throw new SpreadsheetException('Value could not be bound to cell.'); + } + + return $this; + } + + /** + * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder). + * + * @param mixed $value Value + * @param string $dataType Explicit data type, see DataType::TYPE_* + * This parameter is currently optional (default = string). + * Omitting it is ***DEPRECATED***, and the default will be removed in a future release. + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatype, then the value you enter may be changed to match the datatype + * that you specify. + * + * @throws SpreadsheetException + */ + public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self + { + $oldValue = $this->value; + $quotePrefix = false; + + // set the value according to data type + switch ($dataType) { + case DataType::TYPE_NULL: + $this->value = null; + + break; + case DataType::TYPE_STRING2: + $dataType = DataType::TYPE_STRING; + // no break + case DataType::TYPE_STRING: + // Synonym for string + if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + $quotePrefix = true; + } + // no break + case DataType::TYPE_INLINE: + // Rich text + $value2 = StringHelper::convertToString($value, true); + // Cells?->Worksheet?->Spreadsheet + $binder = $this->parent?->getParent()?->getParent()?->getValueBinder(); + $preserveCr = false; + if ($binder !== null && method_exists($binder, 'getPreserveCr')) { + /** @var bool */ + $preserveCr = $binder->getPreserveCr(); + } + $this->value = DataType::checkString(($value instanceof RichText) ? $value : $value2, $preserveCr); + + break; + case DataType::TYPE_NUMERIC: + if ($value !== null && !is_bool($value) && !is_numeric($value)) { + throw new SpreadsheetException('Invalid numeric value for datatype Numeric'); + } + $this->value = 0 + $value; + + break; + case DataType::TYPE_FORMULA: + $this->value = StringHelper::convertToString($value, true); + + break; + case DataType::TYPE_BOOL: + $this->value = (bool) $value; + + break; + case DataType::TYPE_ISO_DATE: + $this->value = SharedDate::convertIsoDate($value); + $dataType = DataType::TYPE_NUMERIC; + + break; + case DataType::TYPE_ERROR: + $this->value = DataType::checkErrorCode($value); + + break; + default: + throw new SpreadsheetException('Invalid datatype: ' . $dataType); + } + + // set the datatype + $this->dataType = $dataType; + + $this->updateInCollection(); + $cellCoordinate = $this->getCoordinate(); + self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value); + $worksheet = $this->getWorksheet(); + $spreadsheet = $worksheet->getParent(); + if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) { + $originalSelected = $worksheet->getSelectedCells(); + $activeSheetIndex = $spreadsheet->getActiveSheetIndex(); + $style = $this->getStyle(); + $oldQuotePrefix = $style->getQuotePrefix(); + if ($oldQuotePrefix !== $quotePrefix) { + $style->setQuotePrefix($quotePrefix); + } + $worksheet->setSelectedCells($originalSelected); + if ($activeSheetIndex >= 0) { + $spreadsheet->setActiveSheetIndex($activeSheetIndex); + } + } + + return $this->getParent()?->get($cellCoordinate) ?? $this; + } + + public const CALCULATE_DATE_TIME_ASIS = 0; + public const CALCULATE_DATE_TIME_FLOAT = 1; + public const CALCULATE_TIME_FLOAT = 2; + + private static int $calculateDateTimeType = self::CALCULATE_DATE_TIME_ASIS; + + public static function getCalculateDateTimeType(): int + { + return self::$calculateDateTimeType; + } + + /** @throws CalculationException */ + public static function setCalculateDateTimeType(int $calculateDateTimeType): void + { + self::$calculateDateTimeType = match ($calculateDateTimeType) { + self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType, + default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"), + }; + } + + /** + * Convert date, time, or datetime from int to float if desired. + */ + private function convertDateTimeInt(mixed $result): mixed + { + if (is_int($result)) { + if (self::$calculateDateTimeType === self::CALCULATE_TIME_FLOAT) { + if (SharedDate::isDateTime($this, $result, false)) { + $result = (float) $result; + } + } elseif (self::$calculateDateTimeType === self::CALCULATE_DATE_TIME_FLOAT) { + if (SharedDate::isDateTime($this, $result, true)) { + $result = (float) $result; + } + } + } + + return $result; + } + + /** + * Get calculated cell value converted to string. + */ + public function getCalculatedValueString(): string + { + $value = $this->getCalculatedValue(); + while (is_array($value)) { + $value = array_shift($value); + } + + return StringHelper::convertToString($value, false); + } + + /** + * Get calculated cell value. + * + * @param bool $resetLog Whether the calculation engine logger should be reset or not + * + * @throws CalculationException + */ + public function getCalculatedValue(bool $resetLog = true): mixed + { + $title = 'unknown'; + $oldAttributes = $this->formulaAttributes; + $oldAttributesT = $oldAttributes['t'] ?? ''; + $coordinate = $this->getCoordinate(); + $oldAttributesRef = $oldAttributes['ref'] ?? $coordinate; + $originalValue = $this->value; + $originalDataType = $this->dataType; + $this->formulaAttributes = []; + $spill = false; + + if ($this->dataType === DataType::TYPE_FORMULA) { + try { + $currentCalendar = SharedDate::getExcelCalendar(); + SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar()); + $thisworksheet = $this->getWorksheet(); + $index = $thisworksheet->getParentOrThrow()->getActiveSheetIndex(); + $selected = $thisworksheet->getSelectedCells(); + $title = $thisworksheet->getTitle(); + $calculation = Calculation::getInstance($thisworksheet->getParent()); + $result = $calculation->calculateCellValue($this, $resetLog); + $result = $this->convertDateTimeInt($result); + $thisworksheet->setSelectedCells($selected); + $thisworksheet->getParentOrThrow()->setActiveSheetIndex($index); + if (is_array($result) && $calculation->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) { + while (is_array($result)) { + $result = array_shift($result); + } + } + if ( + !is_array($result) + && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY + && $oldAttributesT === 'array' + && ($oldAttributesRef === $coordinate || $oldAttributesRef === "$coordinate:$coordinate") + ) { + $result = [$result]; + } + // if return_as_array for formula like '=sheet!cell' + if (is_array($result) && count($result) === 1) { + $resultKey = array_keys($result)[0]; + $resultValue = $result[$resultKey]; + if (is_int($resultKey) && is_array($resultValue) && count($resultValue) === 1) { + $resultKey2 = array_keys($resultValue)[0]; + $resultValue2 = $resultValue[$resultKey2]; + if (is_string($resultKey2) && !is_array($resultValue2) && preg_match('/[a-zA-Z]{1,3}/', $resultKey2) === 1) { + $result = $resultValue2; + } + } + } + $newColumn = $this->getColumn(); + if (is_array($result)) { + $result = self::convertSpecialArray($result); + $this->formulaAttributes['t'] = 'array'; + $this->formulaAttributes['ref'] = $maxCoordinate = $coordinate; + $newRow = $row = $this->getRow(); + $column = $this->getColumn(); + foreach ($result as $resultRow) { + if (is_array($resultRow)) { + $newColumn = $column; + foreach ($resultRow as $resultValue) { + if ($row !== $newRow || $column !== $newColumn) { + $maxCoordinate = $newColumn . $newRow; + if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) { + if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) { + $spill = true; + + break; + } + } + } + /** @var string $newColumn */ + StringHelper::stringIncrement($newColumn); + } + ++$newRow; + } else { + if ($row !== $newRow || $column !== $newColumn) { + $maxCoordinate = $newColumn . $newRow; + if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) { + if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) { + $spill = true; + } + } + } + StringHelper::stringIncrement($newColumn); + } + if ($spill) { + break; + } + } + if (!$spill) { + $this->formulaAttributes['ref'] .= ":$maxCoordinate"; + } + $thisworksheet->getCell($column . $row); + } + if (is_array($result)) { + if ($oldAttributes !== null && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) { + if (($oldAttributesT) === 'array') { + $thisworksheet = $this->getWorksheet(); + $coordinate = $this->getCoordinate(); + $ref = $oldAttributesRef; + if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) { + if (isset($matches[3])) { + $minCol = $matches[1]; + $minRow = (int) $matches[2]; + $maxCol = $matches[4]; + StringHelper::stringIncrement($maxCol); + $maxRow = (int) $matches[5]; + for ($row = $minRow; $row <= $maxRow; ++$row) { + for ($col = $minCol; $col !== $maxCol; StringHelper::stringIncrement($col)) { + /** @var string $col */ + if ("$col$row" !== $coordinate) { + $thisworksheet->getCell("$col$row")->setValue(null); + } + } + } + } + } + $thisworksheet->getCell($coordinate); + } + } + } + if ($spill) { + $result = ExcelError::SPILL(); + } + if (is_array($result)) { + $newRow = $row = $this->getRow(); + $newColumn = $column = $this->getColumn(); + foreach ($result as $resultRow) { + if (is_array($resultRow)) { + $newColumn = $column; + foreach ($resultRow as $resultValue) { + if ($row !== $newRow || $column !== $newColumn) { + $thisworksheet + ->getCell($newColumn . $newRow) + ->setValue($resultValue); + } + StringHelper::stringIncrement($newColumn); + } + ++$newRow; + } else { + if ($row !== $newRow || $column !== $newColumn) { + $thisworksheet->getCell($newColumn . $newRow)->setValue($resultRow); + } + StringHelper::stringIncrement($newColumn); + } + } + $thisworksheet->getCell($column . $row); + $this->value = $originalValue; + $this->dataType = $originalDataType; + } + } catch (SpreadsheetException $ex) { + SharedDate::setExcelCalendar($currentCalendar); + if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { + return $this->calculatedValue; // Fallback for calculations referencing external files. + } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { + return ExcelError::NAME(); + } + + throw new CalculationException( + $title . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(), + $ex->getCode(), + $ex + ); + } + SharedDate::setExcelCalendar($currentCalendar); + + if ($result === Functions::NOT_YET_IMPLEMENTED) { + $this->formulaAttributes = $oldAttributes; + + return $this->calculatedValue; // Fallback if calculation engine does not support the formula. + } + + return $result; + } elseif ($this->value instanceof RichText) { + return $this->value->getPlainText(); + } + + return $this->convertDateTimeInt($this->value); + } + + /** + * Convert array like the following (preserve values, lose indexes): + * [ + * rowNumber1 => [colLetter1 => value, colLetter2 => value ...], + * rowNumber2 => [colLetter1 => value, colLetter2 => value ...], + * ... + * ]. + * + * @param mixed[] $array + * + * @return mixed[] + */ + private static function convertSpecialArray(array $array): array + { + $newArray = []; + foreach ($array as $rowIndex => $row) { + if (!is_int($rowIndex) || $rowIndex <= 0 || !is_array($row)) { + return $array; + } + $keys = array_keys($row); + $key0 = $keys[0] ?? ''; + if (!is_string($key0)) { + return $array; + } + $newArray[] = array_values($row); + } + + return $newArray; + } + + /** + * Set old calculated value (cached). + * + * @param mixed $originalValue Value + */ + public function setCalculatedValue(mixed $originalValue, bool $tryNumeric = true): self + { + if ($originalValue !== null) { + $this->calculatedValue = ($tryNumeric && is_numeric($originalValue)) ? (0 + $originalValue) : $originalValue; + } + + return $this->updateInCollection(); + } + + /** + * Get old calculated value (cached) + * This returns the value last calculated by MS Excel or whichever spreadsheet program was used to + * create the original spreadsheet file. + * Note that this value is not guaranteed to reflect the actual calculated value because it is + * possible that auto-calculation was disabled in the original spreadsheet, and underlying data + * values used by the formula have changed since it was last calculated. + */ + public function getOldCalculatedValue(): mixed + { + return $this->calculatedValue; + } + + /** + * Get cell data type. + */ + public function getDataType(): string + { + return $this->dataType; + } + + /** + * Set cell data type. + * + * @param string $dataType see DataType::TYPE_* + */ + public function setDataType(string $dataType): self + { + $this->setValueExplicit($this->value, $dataType); + + return $this; + } + + /** + * Identify if the cell contains a formula. + */ + public function isFormula(): bool + { + return $this->dataType === DataType::TYPE_FORMULA && $this->getStyle()->getQuotePrefix() === false; + } + + /** + * Does this cell contain Data validation rules? + * + * @throws SpreadsheetException + */ + public function hasDataValidation(): bool + { + if (!isset($this->parent)) { + throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet'); + } + + return $this->getWorksheet()->dataValidationExists($this->getCoordinate()); + } + + /** + * Get Data validation rules. + * + * @throws SpreadsheetException + */ + public function getDataValidation(): DataValidation + { + if (!isset($this->parent)) { + throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet'); + } + + return $this->getWorksheet()->getDataValidation($this->getCoordinate()); + } + + /** + * Set Data validation rules. + * + * @throws SpreadsheetException + */ + public function setDataValidation(?DataValidation $dataValidation = null): self + { + if (!isset($this->parent)) { + throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet'); + } + + $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation); + + return $this->updateInCollection(); + } + + /** + * Does this cell contain valid value? + */ + public function hasValidValue(): bool + { + $validator = new DataValidator(); + + return $validator->isValid($this); + } + + /** + * Does this cell contain a Hyperlink? + * + * @throws SpreadsheetException + */ + public function hasHyperlink(): bool + { + if (!isset($this->parent)) { + throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet'); + } + + return $this->getWorksheet()->hyperlinkExists($this->getCoordinate()); + } + + /** + * Get Hyperlink. + * + * @throws SpreadsheetException + */ + public function getHyperlink(): Hyperlink + { + if (!isset($this->parent)) { + throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet'); + } + + return $this->getWorksheet()->getHyperlink($this->getCoordinate()); + } + + /** + * Set Hyperlink. + * + * @throws SpreadsheetException + */ + public function setHyperlink(?Hyperlink $hyperlink = null): self + { + if (!isset($this->parent)) { + throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet'); + } + + $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink); + + return $this->updateInCollection(); + } + + /** + * Get cell collection. + */ + public function getParent(): ?Cells + { + return $this->parent; + } + + /** + * Get parent worksheet. + * + * @throws SpreadsheetException + */ + public function getWorksheet(): Worksheet + { + $parent = $this->parent; + if ($parent !== null) { + $worksheet = $parent->getParent(); + } else { + $worksheet = null; + } + + if ($worksheet === null) { + throw new SpreadsheetException('Worksheet no longer exists'); + } + + return $worksheet; + } + + public function getWorksheetOrNull(): ?Worksheet + { + $parent = $this->parent; + if ($parent !== null) { + $worksheet = $parent->getParent(); + } else { + $worksheet = null; + } + + return $worksheet; + } + + /** + * Is this cell in a merge range. + */ + public function isInMergeRange(): bool + { + return (bool) $this->getMergeRange(); + } + + /** + * Is this cell the master (top left cell) in a merge range (that holds the actual data value). + */ + public function isMergeRangeValueCell(): bool + { + if ($mergeRange = $this->getMergeRange()) { + $mergeRange = Coordinate::splitRange($mergeRange); + [$startCell] = $mergeRange[0]; + + return $this->getCoordinate() === $startCell; + } + + return false; + } + + /** + * If this cell is in a merge range, then return the range. + * + * @return false|string + */ + public function getMergeRange() + { + foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) { + if ($this->isInRange($mergeRange)) { + return $mergeRange; + } + } + + return false; + } + + /** + * Get cell style. + */ + public function getStyle(): Style + { + return $this->getWorksheet()->getStyle($this->getCoordinate()); + } + + /** + * Get cell style. + */ + public function getAppliedStyle(): Style + { + if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) { + return $this->getStyle(); + } + $range = $this->getWorksheet()->getConditionalRange($this->getCoordinate()); + if ($range === null) { + return $this->getStyle(); + } + + $matcher = new CellStyleAssessor($this, $range); + + return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate())); + } + + /** + * Re-bind parent. + */ + public function rebindParent(Worksheet $parent): self + { + $this->parent = $parent->getCellCollection(); + + return $this->updateInCollection(); + } + + /** + * Is cell in a specific range? + * + * @param string $range Cell range (e.g. A1:A1) + */ + public function isInRange(string $range): bool + { + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range); + + // Translate properties + $myColumn = Coordinate::columnIndexFromString($this->getColumn()); + $myRow = $this->getRow(); + + // Verify if cell is in range + return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn) + && ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow); + } + + /** + * Compare 2 cells. + * + * @param Cell $a Cell a + * @param Cell $b Cell b + * + * @return int Result of comparison (always -1 or 1, never zero!) + */ + public static function compareCells(self $a, self $b): int + { + if ($a->getRow() < $b->getRow()) { + return -1; + } elseif ($a->getRow() > $b->getRow()) { + return 1; + } elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) { + return -1; + } + + return 1; + } + + /** + * Get value binder to use. + */ + public static function getValueBinder(): IValueBinder + { + if (self::$valueBinder === null) { + self::$valueBinder = new DefaultValueBinder(); + } + + return self::$valueBinder; + } + + /** + * Set value binder to use. + */ + public static function setValueBinder(IValueBinder $binder): void + { + self::$valueBinder = $binder; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $propertyName => $propertyValue) { + if ((is_object($propertyValue)) && ($propertyName !== 'parent')) { + $this->$propertyName = clone $propertyValue; + } else { + $this->$propertyName = $propertyValue; + } + } + } + + /** + * Get index to cellXf. + */ + public function getXfIndex(): int + { + return $this->xfIndex; + } + + /** + * Set index to cellXf. + */ + public function setXfIndex(int $indexValue): self + { + $this->xfIndex = $indexValue; + + return $this->updateInCollection(); + } + + /** + * Set the formula attributes. + * + * @param null|array $attributes + */ + public function setFormulaAttributes(?array $attributes): self + { + $this->formulaAttributes = $attributes; + + return $this; + } + + /** + * Get the formula attributes. + * + * @return null|array + */ + public function getFormulaAttributes(): mixed + { + return $this->formulaAttributes; + } + + /** + * Convert to string. + */ + public function __toString(): string + { + $retVal = $this->value; + + return StringHelper::convertToString($retVal, false); + } + + public function getIgnoredErrors(): IgnoredErrors + { + return $this->ignoredErrors; + } + + public function isLocked(): bool + { + $protected = $this->parent?->getParent()?->getProtection()?->getSheet(); + if ($protected !== true) { + return false; + } + $locked = $this->getStyle()->getProtection()->getLocked(); + + return $locked !== Protection::PROTECTION_UNPROTECTED; + } + + public function isHiddenOnFormulaBar(): bool + { + if ($this->getDataType() !== DataType::TYPE_FORMULA) { + return false; + } + $protected = $this->parent?->getParent()?->getProtection()?->getSheet(); + if ($protected !== true) { + return false; + } + $hidden = $this->getStyle()->getProtection()->getHidden(); + + return $hidden !== Protection::PROTECTION_UNPROTECTED; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/CellAddress.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/CellAddress.php new file mode 100644 index 0000000..a4a518e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/CellAddress.php @@ -0,0 +1,149 @@ +cellAddress = str_replace('$', '', $cellAddress); + [$this->columnId, $this->rowId, $this->columnName] = Coordinate::indexesFromString($this->cellAddress); + $this->worksheet = $worksheet; + } + + public function __destruct() + { + unset($this->worksheet); + } + + /** + * @phpstan-assert int|numeric-string $columnId + * @phpstan-assert int|numeric-string $rowId + */ + private static function validateColumnAndRow(int|string $columnId, int|string $rowId): void + { + if (!is_numeric($columnId) || $columnId <= 0 || !is_numeric($rowId) || $rowId <= 0) { + throw new Exception('Row and Column Ids must be positive integer values'); + } + } + + public static function fromColumnAndRow(int|string $columnId, int|string $rowId, ?Worksheet $worksheet = null): self + { + self::validateColumnAndRow($columnId, $rowId); + + return new self(Coordinate::stringFromColumnIndex($columnId) . $rowId, $worksheet); + } + + /** @param array $array */ + public static function fromColumnRowArray(array $array, ?Worksheet $worksheet = null): self + { + [$columnId, $rowId] = $array; + + return self::fromColumnAndRow($columnId, $rowId, $worksheet); + } + + public static function fromCellAddress(string $cellAddress, ?Worksheet $worksheet = null): self + { + return new self($cellAddress, $worksheet); + } + + /** + * The returned address string will contain the worksheet name as well, if available, + * (ie. if a Worksheet was provided to the constructor). + * e.g. "'Mark''s Worksheet'!C5". + */ + public function fullCellAddress(): string + { + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$this->cellAddress}"; + } + + return $this->cellAddress; + } + + public function worksheet(): ?Worksheet + { + return $this->worksheet; + } + + /** + * The returned address string will contain just the column/row address, + * (even if a Worksheet was provided to the constructor). + * e.g. "C5". + */ + public function cellAddress(): string + { + return $this->cellAddress; + } + + public function rowId(): int + { + return $this->rowId; + } + + public function columnId(): int + { + return $this->columnId; + } + + public function columnName(): string + { + return $this->columnName; + } + + public function nextRow(int $offset = 1): self + { + $newRowId = $this->rowId + $offset; + if ($newRowId < 1) { + $newRowId = 1; + } + + return self::fromColumnAndRow($this->columnId, $newRowId); + } + + public function previousRow(int $offset = 1): self + { + return $this->nextRow(0 - $offset); + } + + public function nextColumn(int $offset = 1): self + { + $newColumnId = $this->columnId + $offset; + if ($newColumnId < 1) { + $newColumnId = 1; + } + + return self::fromColumnAndRow($newColumnId, $this->rowId); + } + + public function previousColumn(int $offset = 1): self + { + return $this->nextColumn(0 - $offset); + } + + /** + * The returned address string will contain the worksheet name as well, if available, + * (ie. if a Worksheet was provided to the constructor). + * e.g. "'Mark''s Worksheet'!C5". + */ + public function __toString(): string + { + return $this->fullCellAddress(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/CellRange.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/CellRange.php new file mode 100644 index 0000000..677009d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/CellRange.php @@ -0,0 +1,134 @@ + + */ +class CellRange implements AddressRange, Stringable +{ + protected CellAddress $from; + + protected CellAddress $to; + + public function __construct(CellAddress $from, CellAddress $to) + { + $this->validateFromTo($from, $to); + } + + private function validateFromTo(CellAddress $from, CellAddress $to): void + { + // Identify actual top-left and bottom-right values (in case we've been given top-right and bottom-left) + $firstColumn = min($from->columnId(), $to->columnId()); + $firstRow = min($from->rowId(), $to->rowId()); + $lastColumn = max($from->columnId(), $to->columnId()); + $lastRow = max($from->rowId(), $to->rowId()); + + $fromWorksheet = $from->worksheet(); + $toWorksheet = $to->worksheet(); + $this->validateWorksheets($fromWorksheet, $toWorksheet); + + $this->from = $this->cellAddressWrapper($firstColumn, $firstRow, $fromWorksheet); + $this->to = $this->cellAddressWrapper($lastColumn, $lastRow, $toWorksheet); + } + + private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void + { + if ($fromWorksheet !== null && $toWorksheet !== null) { + // We could simply compare worksheets rather than worksheet titles; but at some point we may introduce + // support for 3d ranges; and at that point we drop this check and let the validation fall through + // to the check for same workbook; but unless we check on titles, this test will also detect if the + // worksheets are in different spreadsheets, and the next check will never execute or throw its + // own exception. + if ($fromWorksheet->getTitle() !== $toWorksheet->getTitle()) { + throw new Exception('3d Cell Ranges are not supported'); + } elseif ($fromWorksheet->getParent() !== $toWorksheet->getParent()) { + throw new Exception('Worksheets must be in the same spreadsheet'); + } + } + } + + private function cellAddressWrapper(int $column, int $row, ?Worksheet $worksheet = null): CellAddress + { + $cellAddress = Coordinate::stringFromColumnIndex($column) . (string) $row; + + return new class ($cellAddress, $worksheet) extends CellAddress { + public function nextRow(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::nextRow($offset); + $this->rowId = $result->rowId; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function previousRow(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::previousRow($offset); + $this->rowId = $result->rowId; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function nextColumn(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::nextColumn($offset); + $this->columnId = $result->columnId; + $this->columnName = $result->columnName; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function previousColumn(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::previousColumn($offset); + $this->columnId = $result->columnId; + $this->columnName = $result->columnName; + $this->cellAddress = $result->cellAddress; + + return $this; + } + }; + } + + public function from(): CellAddress + { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + + return $this->from; + } + + public function to(): CellAddress + { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + + return $this->to; + } + + public function __toString(): string + { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + + if ($this->from->cellAddress() === $this->to->cellAddress()) { + return "{$this->from->fullCellAddress()}"; + } + + $fromAddress = $this->from->fullCellAddress(); + $toAddress = $this->to->cellAddress(); + + return "{$fromAddress}:{$toAddress}"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/ColumnRange.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/ColumnRange.php new file mode 100644 index 0000000..7f32a05 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/ColumnRange.php @@ -0,0 +1,125 @@ + + */ +class ColumnRange implements AddressRange, Stringable +{ + protected ?Worksheet $worksheet; + + protected int $from; + + protected int $to; + + public function __construct(string $from, ?string $to = null, ?Worksheet $worksheet = null) + { + $this->validateFromTo( + Coordinate::columnIndexFromString($from), + Coordinate::columnIndexFromString($to ?? $from) + ); + $this->worksheet = $worksheet; + } + + public function __destruct() + { + $this->worksheet = null; + } + + public static function fromColumnIndexes(int $from, int $to, ?Worksheet $worksheet = null): self + { + return new self(Coordinate::stringFromColumnIndex($from), Coordinate::stringFromColumnIndex($to), $worksheet); + } + + /** + * @param array $array + */ + public static function fromArray(array $array, ?Worksheet $worksheet = null): self + { + array_walk( + $array, + function (&$column): void { + $column = is_numeric($column) ? Coordinate::stringFromColumnIndex((int) $column) : $column; + } + ); + /** @var string $from */ + /** @var string $to */ + [$from, $to] = $array; + + return new self($from, $to, $worksheet); + } + + private function validateFromTo(int $from, int $to): void + { + // Identify actual top and bottom values (in case we've been given bottom and top) + $this->from = min($from, $to); + $this->to = max($from, $to); + } + + public function columnCount(): int + { + return $this->to - $this->from + 1; + } + + public function shiftDown(int $offset = 1): self + { + $newFrom = $this->from + $offset; + $newFrom = ($newFrom < 1) ? 1 : $newFrom; + + $newTo = $this->to + $offset; + $newTo = ($newTo < 1) ? 1 : $newTo; + + return self::fromColumnIndexes($newFrom, $newTo, $this->worksheet); + } + + public function shiftUp(int $offset = 1): self + { + return $this->shiftDown(0 - $offset); + } + + public function from(): string + { + return Coordinate::stringFromColumnIndex($this->from); + } + + public function to(): string + { + return Coordinate::stringFromColumnIndex($this->to); + } + + public function fromIndex(): int + { + return $this->from; + } + + public function toIndex(): int + { + return $this->to; + } + + public function toCellRange(): CellRange + { + return new CellRange( + CellAddress::fromColumnAndRow($this->from, 1, $this->worksheet), + CellAddress::fromColumnAndRow($this->to, AddressRange::MAX_ROW) + ); + } + + public function __toString(): string + { + $from = $this->from(); + $to = $this->to(); + + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$from}:{$to}"; + } + + return "{$from}:{$to}"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Coordinate.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Coordinate.php new file mode 100644 index 0000000..5aa96f4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Coordinate.php @@ -0,0 +1,776 @@ +\$?[A-Z]{1,3})(?\$?\d{1,7})$/i'; + public const FULL_REFERENCE_REGEX = '/^(?:(?[^!]*)!)?(?(?[$]?[A-Z]{1,3}[$]?\d{1,7})(?:\:(?[$]?[A-Z]{1,3}[$]?\d{1,7}))?)$/i'; + + /** + * Default range variable constant. + * + * @var string + */ + const DEFAULT_RANGE = 'A1:A1'; + + /** + * Convert string coordinate to [0 => int column index, 1 => int row index]. + * + * @param string $cellAddress eg: 'A1' + * + * @return array{0: string, 1: string} Array containing column and row (indexes 0 and 1) + */ + public static function coordinateFromString(string $cellAddress): array + { + if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) { + return [$matches['col'], $matches['row']]; + } elseif (self::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells'); + } elseif ($cellAddress == '') { + throw new Exception('Cell coordinate can not be zero-length string'); + } + + throw new Exception('Invalid cell coordinate ' . $cellAddress); + } + + /** + * Convert string coordinate to [0 => int column index, 1 => int row index, 2 => string column string]. + * + * @param string $coordinates eg: 'A1', '$B$12' + * + * @return array{0: int, 1: int, 2: string} Array containing column and row index, and column string + */ + public static function indexesFromString(string $coordinates): array + { + [$column, $row] = self::coordinateFromString($coordinates); + $column = ltrim($column, '$'); + + return [ + self::columnIndexFromString($column), + (int) ltrim($row, '$'), + $column, + ]; + } + + /** + * Checks if a Cell Address represents a range of cells. + * + * @param string $cellAddress eg: 'A1' or 'A1:A2' or 'A1:A2,C1:C2' + * + * @return bool Whether the coordinate represents a range of cells + */ + public static function coordinateIsRange(string $cellAddress): bool + { + return str_contains($cellAddress, ':') || str_contains($cellAddress, ','); + } + + /** + * Make string row, column or cell coordinate absolute. + * + * @param int|string $cellAddress e.g. 'A' or '1' or 'A1' + * Note that this value can be a row or column reference as well as a cell reference + * + * @return string Absolute coordinate e.g. '$A' or '$1' or '$A$1' + */ + public static function absoluteReference(int|string $cellAddress): string + { + $cellAddress = (string) $cellAddress; + if (self::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells'); + } + + // Split out any worksheet name from the reference + [$worksheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + if ($worksheet > '') { + $worksheet .= '!'; + } + + // Create absolute coordinate + $cellAddress = "$cellAddress"; + if (ctype_digit($cellAddress)) { + return $worksheet . '$' . $cellAddress; + } elseif (ctype_alpha($cellAddress)) { + return $worksheet . '$' . strtoupper($cellAddress); + } + + return $worksheet . self::absoluteCoordinate($cellAddress); + } + + /** + * Make string coordinate absolute. + * + * @param string $cellAddress e.g. 'A1' + * + * @return string Absolute coordinate e.g. '$A$1' + */ + public static function absoluteCoordinate(string $cellAddress): string + { + if (self::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells'); + } + + // Split out any worksheet name from the coordinate + [$worksheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + if ($worksheet > '') { + $worksheet .= '!'; + } + + // Create absolute coordinate + [$column, $row] = self::coordinateFromString($cellAddress ?? 'A1'); + $column = ltrim($column, '$'); + $row = ltrim($row, '$'); + + return $worksheet . '$' . $column . '$' . $row; + } + + /** + * Split range into coordinate strings, using comma for union + * and ignoring intersection (space). + * + * @param string $range e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4' + * + * @return array> Array containing one or more arrays containing one or two coordinate strings + * e.g. ['B4','D9'] or [['B4','D9'], ['H2','O11']] + * or ['B4'] + */ + public static function splitRange(string $range): array + { + // Ensure $pRange is a valid range + if (empty($range)) { + $range = self::DEFAULT_RANGE; + } + + $exploded = explode(',', $range); + $outArray = []; + foreach ($exploded as $value) { + $outArray[] = explode(':', $value); + } + + return $outArray; + } + + /** + * Split range into coordinate strings, resolving unions and intersections. + * + * @param string $range e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4' + * @param bool $unionIsComma true=comma is union, space is intersection + * false=space is union, comma is intersection + * + * @return array> Array containing one or more arrays containing one or two coordinate strings + * e.g. ['B4','D9'] or [['B4','D9'], ['H2','O11']] + * or ['B4'] + */ + public static function allRanges(string $range, bool $unionIsComma = true): array + { + if (!$unionIsComma) { + $range = str_replace([',', ' ', "\0"], ["\0", ',', ' '], $range); + } + + return self::splitRange( + self::resolveUnionAndIntersection($range) + ); + } + + /** + * Build range from coordinate strings. + * + * @param mixed[] $range Array containing one or more arrays containing one or two coordinate strings + * + * @return string String representation of $pRange + */ + public static function buildRange(array $range): string + { + // Verify range + if (empty($range)) { + throw new Exception('Range does not contain any information'); + } + + // Build range + $counter = count($range); + for ($i = 0; $i < $counter; ++$i) { + if (!is_array($range[$i])) { + throw new Exception('Each array entry must be an array'); + } + $range[$i] = implode(':', $range[$i]); + } + + return implode(',', $range); + } + + /** + * Calculate range boundaries. + * + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) + * + * @return array{array{int, int}, array{int, int}} Range coordinates [Start Cell, End Cell] + * where Start Cell and End Cell are arrays (Column Number, Row Number) + */ + public static function rangeBoundaries(string $range): array + { + // Ensure $pRange is a valid range + if (empty($range)) { + $range = self::DEFAULT_RANGE; + } + + // Uppercase coordinate + $range = strtoupper($range); + + // Extract range + if (!str_contains($range, ':')) { + $rangeA = $rangeB = $range; + } else { + [$rangeA, $rangeB] = explode(':', $range); + } + + if (is_numeric($rangeA) && is_numeric($rangeB)) { + $rangeA = 'A' . $rangeA; + $rangeB = AddressRange::MAX_COLUMN . $rangeB; + } + + if (ctype_alpha($rangeA) && ctype_alpha($rangeB)) { + $rangeA = $rangeA . '1'; + $rangeB = $rangeB . AddressRange::MAX_ROW; + } + + // Calculate range outer borders + $rangeStart = self::coordinateFromString($rangeA); + $rangeEnd = self::coordinateFromString($rangeB); + + // Translate column into index + $rangeStart[0] = self::columnIndexFromString($rangeStart[0]); + $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]); + $rangeStart[1] = (int) $rangeStart[1]; + $rangeEnd[1] = (int) $rangeEnd[1]; + + return [$rangeStart, $rangeEnd]; + } + + /** + * Calculate range dimension. + * + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) + * + * @return array{int, int} Range dimension (width, height) + */ + public static function rangeDimension(string $range): array + { + // Calculate range outer borders + [$rangeStart, $rangeEnd] = self::rangeBoundaries($range); + + return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)]; + } + + /** + * Calculate range boundaries. + * + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) + * + * @return array{array{string, int}, array{string, int}} Range coordinates [Start Cell, End Cell] + * where Start Cell and End Cell are arrays [Column ID, Row Number] + */ + public static function getRangeBoundaries(string $range): array + { + [$rangeA, $rangeB] = self::rangeBoundaries($range); + + return [ + [self::stringFromColumnIndex($rangeA[0]), $rangeA[1]], + [self::stringFromColumnIndex($rangeB[0]), $rangeB[1]], + ]; + } + + /** + * Check if cell or range reference is valid and return an array with type of reference (cell or range), worksheet (if it was given) + * and the coordinate or the first coordinate and second coordinate if it is a range. + * + * @param string $reference Coordinate or Range (e.g. A1:A1, B2, B:C, 2:3) + * + * @return array{type: string, firstCoordinate?: string, secondCoordinate?: string, coordinate?: string, worksheet?: string, localReference?: string} reference data + */ + private static function validateReferenceAndGetData($reference): array + { + $data = []; + if (1 !== preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches)) { + return ['type' => 'invalid']; + } + + if (isset($matches['secondCoordinate'])) { + $data['type'] = 'range'; + $data['firstCoordinate'] = str_replace('$', '', $matches['firstCoordinate']); + $data['secondCoordinate'] = str_replace('$', '', $matches['secondCoordinate']); + } else { + $data['type'] = 'coordinate'; + $data['coordinate'] = str_replace('$', '', $matches['firstCoordinate']); + } + + $worksheet = $matches['worksheet']; + if ($worksheet !== '') { + if (str_starts_with($worksheet, "'") && str_ends_with($worksheet, "'")) { + $worksheet = substr($worksheet, 1, -1); + } + $data['worksheet'] = strtolower($worksheet); + } + $data['localReference'] = str_replace('$', '', $matches['localReference']); + + return $data; + } + + /** + * Check if coordinate is inside a range. + * + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) + * @param string $coordinate Cell coordinate (e.g. A1) + * + * @return bool true if coordinate is inside range + */ + public static function coordinateIsInsideRange(string $range, string $coordinate): bool + { + $range = Validations::convertWholeRowColumn($range); + $rangeData = self::validateReferenceAndGetData($range); + if ($rangeData['type'] === 'invalid') { + throw new Exception('First argument needs to be a range'); + } + + $coordinateData = self::validateReferenceAndGetData($coordinate); + if ($coordinateData['type'] === 'invalid') { + throw new Exception('Second argument needs to be a single coordinate'); + } + + if (isset($coordinateData['worksheet']) && !isset($rangeData['worksheet'])) { + return false; + } + if (!isset($coordinateData['worksheet']) && isset($rangeData['worksheet'])) { + return false; + } + + if (isset($coordinateData['worksheet'], $rangeData['worksheet'])) { + if ($coordinateData['worksheet'] !== $rangeData['worksheet']) { + return false; + } + } + + if (!isset($rangeData['localReference'])) { + return false; + } + $boundaries = self::rangeBoundaries($rangeData['localReference']); + if (!isset($coordinateData['localReference'])) { + return false; + } + $coordinates = self::indexesFromString($coordinateData['localReference']); + + $columnIsInside = $boundaries[0][0] <= $coordinates[0] && $coordinates[0] <= $boundaries[1][0]; + if (!$columnIsInside) { + return false; + } + $rowIsInside = $boundaries[0][1] <= $coordinates[1] && $coordinates[1] <= $boundaries[1][1]; + if (!$rowIsInside) { + return false; + } + + return true; + } + + /** + * Column index from string. + * + * @param ?string $columnAddress eg 'A' + * + * @return int Column index (A = 1) + */ + public static function columnIndexFromString(?string $columnAddress): int + { + // Using a lookup cache adds a slight memory overhead, but boosts speed + // caching using a static within the method is faster than a class static, + // though it's additional memory overhead + /** @var int[] */ + static $indexCache = []; + $columnAddress = $columnAddress ?? ''; + + if (isset($indexCache[$columnAddress])) { + return $indexCache[$columnAddress]; + } + // It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array + // rather than use ord() and make it case-insensitive to get rid of the strtoupper() as well. + // Because it's a static, there's no significant memory overhead either. + /** @var array */ + static $columnLookup = [ + 'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, + 'K' => 11, 'L' => 12, 'M' => 13, 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, + 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26, + 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, + 'k' => 11, 'l' => 12, 'm' => 13, 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, + 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26, + ]; + + // We also use the language construct isset() rather than the more costly strlen() function to match the + // length of $columnAddress for improved performance + if (isset($columnAddress[0])) { + if (!isset($columnAddress[1])) { + $indexCache[$columnAddress] = $columnLookup[$columnAddress]; + + return $indexCache[$columnAddress]; + } elseif (!isset($columnAddress[2])) { + $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26 + + $columnLookup[$columnAddress[1]]; + + return $indexCache[$columnAddress]; + } elseif (!isset($columnAddress[3])) { + $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676 + + $columnLookup[$columnAddress[1]] * 26 + + $columnLookup[$columnAddress[2]]; + + return $indexCache[$columnAddress]; + } + } + + throw new Exception( + 'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty') + ); + } + + private const LOOKUP_CACHE = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + /** + * String from column index. + * + * @param int|numeric-string $columnIndex Column index (A = 1) + */ + public static function stringFromColumnIndex(int|string $columnIndex): string + { + /** @var string[] */ + static $indexCache = []; + + if (!isset($indexCache[$columnIndex])) { + $indexValue = $columnIndex; + $base26 = ''; + do { + $characterValue = ($indexValue % 26) ?: 26; + $indexValue = ($indexValue - $characterValue) / 26; + $base26 = self::LOOKUP_CACHE[$characterValue] . $base26; + } while ($indexValue > 0); + $indexCache[$columnIndex] = $base26; + } + + return $indexCache[$columnIndex]; + } + + /** + * Extract all cell references in range, which may be comprised of multiple cell ranges. + * + * @param string $cellRange Range: e.g. 'A1' or 'A1:C10' or 'A1:E10,A20:E25' or 'A1:E5 C3:G7' or 'A1:C1,A3:C3 B1:C3' + * + * @return string[] Array containing single cell references + */ + public static function extractAllCellReferencesInRange(string $cellRange): array + { + if (substr_count($cellRange, '!') > 1) { + throw new Exception('3-D Range References are not supported'); + } + + [$worksheet, $cellRange] = Worksheet::extractSheetTitle($cellRange, true); + $quoted = ''; + if ($worksheet) { + $quoted = Worksheet::nameRequiresQuotes($worksheet) ? "'" : ''; + if (str_starts_with($worksheet, "'") && str_ends_with($worksheet, "'")) { + $worksheet = substr($worksheet, 1, -1); + } + $worksheet = str_replace("'", "''", $worksheet); + } + [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange ?? 'A1'); + + $cells = []; + foreach ($ranges as $range) { + /** @var string $range */ + $cells[] = self::getReferencesForCellBlock($range); + } + + /** @var mixed[] */ + $cells = self::processRangeSetOperators($operators, $cells); + + if (empty($cells)) { + return []; + } + + /** @var string[] */ + $cellList = array_merge(...$cells); //* @phpstan-ignore-line + // Unsure how to satisfy phpstan in line above + + $retVal = array_map( + fn (string $cellAddress) => ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress, + self::sortCellReferenceArray($cellList) + ); + + return $retVal; + } + + /** + * @param mixed[] $operators + * @param mixed[][] $cells + * + * @return mixed[] + */ + private static function processRangeSetOperators(array $operators, array $cells): array + { + $operatorCount = count($operators); + for ($offset = 0; $offset < $operatorCount; ++$offset) { + $operator = $operators[$offset]; + if ($operator !== ' ') { + continue; + } + + $cells[$offset] = array_intersect($cells[$offset], $cells[$offset + 1]); + unset($operators[$offset], $cells[$offset + 1]); + $operators = array_values($operators); + $cells = array_values($cells); + --$offset; + --$operatorCount; + } + + return $cells; + } + + /** + * @param string[] $cellList + * + * @return string[] + */ + private static function sortCellReferenceArray(array $cellList): array + { + // Sort the result by column and row + $sortKeys = []; + foreach ($cellList as $coordinate) { + $column = ''; + $row = 0; + sscanf($coordinate, '%[A-Z]%d', $column, $row); + /** @var int $row */ + $key = (--$row * 16384) + self::columnIndexFromString((string) $column); + $sortKeys[$key] = $coordinate; + } + ksort($sortKeys); + + return array_values($sortKeys); + } + + /** + * Get all cell references applying union and intersection. + * + * @param string $cellBlock A cell range e.g. A1:B5,D1:E5 B2:C4 + * + * @return string A string without intersection operator. + * If there was no intersection to begin with, return original argument. + * Otherwise, return cells and/or cell ranges in that range separated by comma. + */ + public static function resolveUnionAndIntersection(string $cellBlock, string $implodeCharacter = ','): string + { + $cellBlock = preg_replace('/ +/', ' ', trim($cellBlock)) ?? $cellBlock; + $cellBlock = preg_replace('/ ,/', ',', $cellBlock) ?? $cellBlock; + $cellBlock = preg_replace('/, /', ',', $cellBlock) ?? $cellBlock; + $array1 = []; + $blocks = explode(',', $cellBlock); + foreach ($blocks as $block) { + $block0 = explode(' ', $block); + if (count($block0) === 1) { + $array1 = array_merge($array1, $block0); + } else { + $blockIdx = -1; + $array2 = []; + foreach ($block0 as $block00) { + ++$blockIdx; + if ($blockIdx === 0) { + $array2 = self::getReferencesForCellBlock($block00); + } else { + $array2 = array_intersect($array2, self::getReferencesForCellBlock($block00)); + } + } + $array1 = array_merge($array1, $array2); + } + } + + return implode($implodeCharacter, $array1); + } + + /** + * Get all cell references for an individual cell block. + * + * @param string $cellBlock A cell range e.g. A4:B5 + * + * @return string[] All individual cells in that range + */ + private static function getReferencesForCellBlock(string $cellBlock): array + { + $returnValue = []; + + // Single cell? + if (!self::coordinateIsRange($cellBlock)) { + return (array) $cellBlock; + } + + // Range... + $ranges = self::splitRange($cellBlock); + foreach ($ranges as $range) { + // Single cell? + if (!isset($range[1])) { + $returnValue[] = $range[0]; + + continue; + } + + // Range... + [$rangeStart, $rangeEnd] = $range; + [$startColumn, $startRow] = self::coordinateFromString($rangeStart); + [$endColumn, $endRow] = self::coordinateFromString($rangeEnd); + $startColumnIndex = self::columnIndexFromString($startColumn); + $endColumnIndex = self::columnIndexFromString($endColumn); + ++$endColumnIndex; + + // Current data + $currentColumnIndex = $startColumnIndex; + $currentRow = $startRow; + + self::validateRange($cellBlock, $startColumnIndex, $endColumnIndex, (int) $currentRow, (int) $endRow); + + // Loop cells + while ($currentColumnIndex < $endColumnIndex) { + /** @var int $currentRow */ + /** @var int $endRow */ + while ($currentRow <= $endRow) { + $returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow; + ++$currentRow; + } + ++$currentColumnIndex; + $currentRow = $startRow; + } + } + + return $returnValue; + } + + /** + * Convert an associative array of single cell coordinates to values to an associative array + * of cell ranges to values. Only adjacent cell coordinates with the same + * value will be merged. If the value is an object, it must implement the method getHashCode(). + * + * For example, this function converts: + * + * [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ] + * + * to: + * + * [ 'A1:A3' => 'x', 'A4' => 'y' ] + * + * @param array $coordinateCollection associative array mapping coordinates to values + * + * @return array associative array mapping coordinate ranges to values + */ + public static function mergeRangesInCollection(array $coordinateCollection): array + { + $hashedValues = []; + $mergedCoordCollection = []; + + foreach ($coordinateCollection as $coord => $value) { + if (self::coordinateIsRange($coord)) { + $mergedCoordCollection[$coord] = $value; + + continue; + } + + [$column, $row] = self::coordinateFromString($coord); + $row = (int) (ltrim($row, '$')); + $hashCode = $column . '-' . StringHelper::convertToString((is_object($value) && method_exists($value, 'getHashCode')) ? $value->getHashCode() : $value); + + if (!isset($hashedValues[$hashCode])) { + $hashedValues[$hashCode] = (object) [ + 'value' => $value, + 'col' => $column, + 'rows' => [$row], + ]; + } else { + $hashedValues[$hashCode]->rows[] = $row; + } + } + + ksort($hashedValues); + + foreach ($hashedValues as $hashedValue) { + sort($hashedValue->rows); + $rowStart = null; + $rowEnd = null; + $ranges = []; + + foreach ($hashedValue->rows as $row) { + if ($rowStart === null) { + $rowStart = $row; + $rowEnd = $row; + } elseif ($rowEnd === $row - 1) { + $rowEnd = $row; + } else { + if ($rowStart == $rowEnd) { + $ranges[] = $hashedValue->col . $rowStart; + } else { + $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd; + } + + $rowStart = $row; + $rowEnd = $row; + } + } + + if ($rowStart !== null) { // @phpstan-ignore-line + if ($rowStart == $rowEnd) { + $ranges[] = $hashedValue->col . $rowStart; + } else { + $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd; + } + } + + foreach ($ranges as $range) { + $mergedCoordCollection[$range] = $hashedValue->value; + } + } + + return $mergedCoordCollection; + } + + /** + * Get the individual cell blocks from a range string, removing any $ characters. + * then splitting by operators and returning an array with ranges and operators. + * + * @return mixed[][] + */ + private static function getCellBlocksFromRangeString(string $rangeString): array + { + $rangeString = str_replace('$', '', strtoupper($rangeString)); + + // split range sets on intersection (space) or union (,) operators + $tokens = preg_split('/([ ,])/', $rangeString, -1, PREG_SPLIT_DELIM_CAPTURE) ?: []; + $split = array_chunk($tokens, 2); + $ranges = array_column($split, 0); + $operators = array_column($split, 1); + + return [$ranges, $operators]; + } + + /** + * Check that the given range is valid, i.e. that the start column and row are not greater than the end column and + * row. + * + * @param string $cellBlock The original range, for displaying a meaningful error message + */ + private static function validateRange(string $cellBlock, int $startColumnIndex, int $endColumnIndex, int $currentRow, int $endRow): void + { + if ($startColumnIndex >= $endColumnIndex || $currentRow > $endRow) { + throw new Exception('Invalid range: "' . $cellBlock . '"'); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataType.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataType.php new file mode 100644 index 0000000..774a57b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataType.php @@ -0,0 +1,92 @@ + + */ + private static array $errorCodes = [ + '#NULL!' => 0, + '#DIV/0!' => 1, + '#VALUE!' => 2, + '#REF!' => 3, + '#NAME?' => 4, + '#NUM!' => 5, + '#N/A' => 6, + '#CALC!' => 7, + ]; + + public const MAX_STRING_LENGTH = 32767; + + /** + * Get list of error codes. + * + * @return array + */ + public static function getErrorCodes(): array + { + return self::$errorCodes; + } + + /** + * Check a string that it satisfies Excel requirements. + * + * @param null|RichText|string $textValue Value to sanitize to an Excel string + * + * @return RichText|string Sanitized value + */ + public static function checkString(null|RichText|string $textValue, bool $preserveCr = false): RichText|string + { + if ($textValue instanceof RichText) { + // TODO: Sanitize Rich-Text string (max. character count is 32,767) + return $textValue; + } + + // string must never be longer than 32,767 characters, truncate if necessary + $textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH); + + // we require that newline is represented as "\n" in core, not as "\r\n" or "\r" + if (!$preserveCr) { + $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); + } + + return $textValue; + } + + /** + * Check a value that it is a valid error code. + * + * @param mixed $value Value to sanitize to an Excel error code + * + * @return string Sanitized value + */ + public static function checkErrorCode(mixed $value): string + { + $default = '#NULL!'; + $value = ($value === null) ? $default : StringHelper::convertToString($value, false, $default); + + if (!isset(self::$errorCodes[$value])) { + $value = $default; + } + + return $value; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataValidation.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataValidation.php new file mode 100644 index 0000000..a21c30c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataValidation.php @@ -0,0 +1,399 @@ +formula1; + } + + /** + * Set Formula 1. + * + * @return $this + */ + public function setFormula1(string $formula): static + { + $this->formula1 = $formula; + + return $this; + } + + /** + * Get Formula 2. + */ + public function getFormula2(): string + { + return $this->formula2; + } + + /** + * Set Formula 2. + * + * @return $this + */ + public function setFormula2(string $formula): static + { + $this->formula2 = $formula; + + return $this; + } + + /** + * Get Type. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Set Type. + * + * @return $this + */ + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } + + /** + * Get Error style. + */ + public function getErrorStyle(): string + { + return $this->errorStyle; + } + + /** + * Set Error style. + * + * @param string $errorStyle see self::STYLE_* + * + * @return $this + */ + public function setErrorStyle(string $errorStyle): static + { + $this->errorStyle = $errorStyle; + + return $this; + } + + /** + * Get Operator. + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * Set Operator. + * + * @return $this + */ + public function setOperator(string $operator): static + { + $this->operator = ($operator === '') ? self::DEFAULT_OPERATOR : $operator; + + return $this; + } + + /** + * Get Allow Blank. + */ + public function getAllowBlank(): bool + { + return $this->allowBlank; + } + + /** + * Set Allow Blank. + * + * @return $this + */ + public function setAllowBlank(bool $allowBlank): static + { + $this->allowBlank = $allowBlank; + + return $this; + } + + /** + * Get Show DropDown. + */ + public function getShowDropDown(): bool + { + return $this->showDropDown; + } + + /** + * Set Show DropDown. + * + * @return $this + */ + public function setShowDropDown(bool $showDropDown): static + { + $this->showDropDown = $showDropDown; + + return $this; + } + + /** + * Get Show InputMessage. + */ + public function getShowInputMessage(): bool + { + return $this->showInputMessage; + } + + /** + * Set Show InputMessage. + * + * @return $this + */ + public function setShowInputMessage(bool $showInputMessage): static + { + $this->showInputMessage = $showInputMessage; + + return $this; + } + + /** + * Get Show ErrorMessage. + */ + public function getShowErrorMessage(): bool + { + return $this->showErrorMessage; + } + + /** + * Set Show ErrorMessage. + * + * @return $this + */ + public function setShowErrorMessage(bool $showErrorMessage): static + { + $this->showErrorMessage = $showErrorMessage; + + return $this; + } + + /** + * Get Error title. + */ + public function getErrorTitle(): string + { + return $this->errorTitle; + } + + /** + * Set Error title. + * + * @return $this + */ + public function setErrorTitle(string $errorTitle): static + { + $this->errorTitle = $errorTitle; + + return $this; + } + + /** + * Get Error. + */ + public function getError(): string + { + return $this->error; + } + + /** + * Set Error. + * + * @return $this + */ + public function setError(string $error): static + { + $this->error = $error; + + return $this; + } + + /** + * Get Prompt title. + */ + public function getPromptTitle(): string + { + return $this->promptTitle; + } + + /** + * Set Prompt title. + * + * @return $this + */ + public function setPromptTitle(string $promptTitle): static + { + $this->promptTitle = $promptTitle; + + return $this; + } + + /** + * Get Prompt. + */ + public function getPrompt(): string + { + return $this->prompt; + } + + /** + * Set Prompt. + * + * @return $this + */ + public function setPrompt(string $prompt): static + { + $this->prompt = $prompt; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + return md5( + $this->formula1 + . $this->formula2 + . $this->type + . $this->errorStyle + . $this->operator + . ($this->allowBlank ? 't' : 'f') + . ($this->showDropDown ? 't' : 'f') + . ($this->showInputMessage ? 't' : 'f') + . ($this->showErrorMessage ? 't' : 'f') + . $this->errorTitle + . $this->error + . $this->promptTitle + . $this->prompt + . $this->sqref + . __CLASS__ + ); + } + + private ?string $sqref = null; + + public function getSqref(): ?string + { + return $this->sqref; + } + + public function setSqref(?string $str): self + { + $this->sqref = $str; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataValidator.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataValidator.php new file mode 100644 index 0000000..5a4c774 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DataValidator.php @@ -0,0 +1,143 @@ +hasDataValidation() || $cell->getDataValidation()->getType() === DataValidation::TYPE_NONE) { + return true; + } + + $cellValue = $cell->getValue(); + $dataValidation = $cell->getDataValidation(); + + if (!$dataValidation->getAllowBlank() && ($cellValue === null || $cellValue === '')) { + return false; + } + + $returnValue = false; + $type = $dataValidation->getType(); + if ($type === DataValidation::TYPE_LIST) { + $returnValue = $this->isValueInList($cell); + } elseif ($type === DataValidation::TYPE_WHOLE) { + if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) { + $returnValue = false; + } else { + $returnValue = $this->numericOperator($dataValidation, (int) $cellValue, $cell); + } + } elseif ($type === DataValidation::TYPE_DECIMAL || $type === DataValidation::TYPE_DATE || $type === DataValidation::TYPE_TIME) { + if (!is_numeric($cellValue)) { + $returnValue = false; + } else { + $returnValue = $this->numericOperator($dataValidation, (float) $cellValue, $cell); + } + } elseif ($type === DataValidation::TYPE_TEXTLENGTH) { + $returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()), $cell); + } + + return $returnValue; + } + + private const TWO_FORMULAS = [DataValidation::OPERATOR_BETWEEN, DataValidation::OPERATOR_NOTBETWEEN]; + + private static function evaluateNumericFormula(mixed $formula, Cell $cell): mixed + { + if (!is_numeric($formula)) { + $calculation = Calculation::getInstance($cell->getWorksheet()->getParent()); + + try { + $formula2 = StringHelper::convertToString($formula); + $result = $calculation + ->calculateFormula("=$formula2", $cell->getCoordinate(), $cell); + while (is_array($result)) { + $result = array_pop($result); + } + $formula = $result; + } catch (Exception) { + // do nothing + } + } + + return $formula; + } + + private function numericOperator(DataValidation $dataValidation, int|float $cellValue, Cell $cell): bool + { + $operator = $dataValidation->getOperator(); + $formula1 = self::evaluateNumericFormula( + $dataValidation->getFormula1(), + $cell + ); + + $formula2 = 0; + if (in_array($operator, self::TWO_FORMULAS, true)) { + $formula2 = self::evaluateNumericFormula( + $dataValidation->getFormula2(), + $cell + ); + } + + return match ($operator) { + DataValidation::OPERATOR_BETWEEN => $cellValue >= $formula1 && $cellValue <= $formula2, + DataValidation::OPERATOR_NOTBETWEEN => $cellValue < $formula1 || $cellValue > $formula2, + DataValidation::OPERATOR_EQUAL => $cellValue == $formula1, + DataValidation::OPERATOR_NOTEQUAL => $cellValue != $formula1, + DataValidation::OPERATOR_LESSTHAN => $cellValue < $formula1, + DataValidation::OPERATOR_LESSTHANOREQUAL => $cellValue <= $formula1, + DataValidation::OPERATOR_GREATERTHAN => $cellValue > $formula1, + DataValidation::OPERATOR_GREATERTHANOREQUAL => $cellValue >= $formula1, + default => false, + }; + } + + /** + * Does this cell contain valid value, based on list? + * + * @param Cell $cell Cell to check the value + */ + private function isValueInList(Cell $cell): bool + { + $cellValueString = $cell->getValueString(); + $dataValidation = $cell->getDataValidation(); + + $formula1 = $dataValidation->getFormula1(); + if (!empty($formula1)) { + // inline values list + if ($formula1[0] === '"') { + return in_array(strtolower($cellValueString), explode(',', strtolower(trim($formula1, '"'))), true); + } + $calculation = Calculation::getInstance($cell->getWorksheet()->getParent()); + + try { + $result = $calculation->calculateFormula("=$formula1", $cell->getCoordinate(), $cell); + $result = is_array($result) ? Functions::flattenArray($result) : [$result]; + foreach ($result as $oneResult) { + if (is_scalar($oneResult) && strcasecmp((string) $oneResult, $cellValueString) === 0) { + return true; + } + } + } catch (Exception) { + // do nothing + } + + return false; + } + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DefaultValueBinder.php new file mode 100644 index 0000000..42a45a9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -0,0 +1,137 @@ +format('Y-m-d H:i:s'); + } elseif ($value instanceof Stringable) { + $value = (string) $value; + } else { + throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value)); + } + + // Set value explicit + $cell->setValueExplicit($value, static::dataTypeForValue($value)); + + // Done! + return true; + } + + /** + * DataType for value. + */ + public static function dataTypeForValue(mixed $value): string + { + // Match the value against a few data types + if ($value === null) { + return DataType::TYPE_NULL; + } + if (is_int($value) && abs($value) > self::FIFTEEN_NINES) { + return DataType::TYPE_STRING; + } + if (is_float($value) || is_int($value)) { + return DataType::TYPE_NUMERIC; + } + if (is_bool($value)) { + return DataType::TYPE_BOOL; + } + if ($value === '') { + return DataType::TYPE_STRING; + } + if ($value instanceof RichText) { + return DataType::TYPE_INLINE; + } + if ($value instanceof Stringable) { + $value = (string) $value; + } + if (!is_string($value)) { + $gettype = is_object($value) ? get_class($value) : gettype($value); + + throw new SpreadsheetException("unusable type $gettype"); + } + if (strlen($value) > 1 && $value[0] === '=') { + $calculation = new Calculation(); + $calculation->disableBranchPruning(); + + try { + if (empty($calculation->parseFormula($value))) { + return DataType::TYPE_STRING; + } + } catch (CalculationException $e) { + $message = $e->getMessage(); + if ( + $message === 'Formula Error: An unexpected error occurred' + || str_contains($message, 'has no operands') + ) { + return DataType::TYPE_STRING; + } + } + + return DataType::TYPE_FORMULA; + } + if (Preg::isMatch('/^[\+\-]?(\d+\.?\d*|\d*\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { + $tValue = ltrim($value, '+-'); + if (strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') { + return DataType::TYPE_STRING; + } + if (!Preg::isMatch('/[eE.]/', $value)) { + $aValue = abs((float) $value); + if ($aValue > self::FIFTEEN_NINES) { + return DataType::TYPE_STRING; + } + } + if (!is_numeric($value)) { + return DataType::TYPE_STRING; + } + + return DataType::TYPE_NUMERIC; + } + $errorCodes = DataType::getErrorCodes(); + if (isset($errorCodes[$value])) { + return DataType::TYPE_ERROR; + } + + return DataType::TYPE_STRING; + } + + protected bool $preserveCr = false; + + public function getPreserveCr(): bool + { + return $this->preserveCr; + } + + public function setPreserveCr(bool $preserveCr): self + { + $this->preserveCr = $preserveCr; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Hyperlink.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Hyperlink.php new file mode 100644 index 0000000..5f1c521 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/Hyperlink.php @@ -0,0 +1,96 @@ +url = $url; + $this->tooltip = $tooltip; + } + + /** + * Get URL. + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * Set URL. + * + * @return $this + */ + public function setUrl(string $url): static + { + $this->url = $url; + + return $this; + } + + /** + * Get tooltip. + */ + public function getTooltip(): string + { + return $this->tooltip; + } + + /** + * Set tooltip. + * + * @return $this + */ + public function setTooltip(string $tooltip): static + { + $this->tooltip = $tooltip; + + return $this; + } + + /** + * Is this hyperlink internal? (to another worksheet or a cell in this worksheet). + */ + public function isInternal(): bool + { + return str_starts_with($this->url, 'sheet://') || str_starts_with($this->url, '#'); + } + + public function getTypeHyperlink(): string + { + return $this->isInternal() ? '' : 'External'; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + return md5( + $this->url + . $this->tooltip + . __CLASS__ + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/IValueBinder.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/IValueBinder.php new file mode 100644 index 0000000..b2f7e91 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/IValueBinder.php @@ -0,0 +1,14 @@ +numberStoredAsText = $value; + + return $this; + } + + public function getNumberStoredAsText(): bool + { + return $this->numberStoredAsText; + } + + public function setFormula(bool $value): self + { + $this->formula = $value; + + return $this; + } + + public function getFormula(): bool + { + return $this->formula; + } + + public function setFormulaRange(bool $value): self + { + $this->formulaRange = $value; + + return $this; + } + + public function getFormulaRange(): bool + { + return $this->formulaRange; + } + + public function setTwoDigitTextYear(bool $value): self + { + $this->twoDigitTextYear = $value; + + return $this; + } + + public function getTwoDigitTextYear(): bool + { + return $this->twoDigitTextYear; + } + + public function setEvalError(bool $value): self + { + $this->evalError = $value; + + return $this; + } + + public function getEvalError(): bool + { + return $this->evalError; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/RowRange.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/RowRange.php new file mode 100644 index 0000000..1d35acc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/RowRange.php @@ -0,0 +1,94 @@ + + */ +class RowRange implements AddressRange, Stringable +{ + protected ?Worksheet $worksheet; + + protected int $from; + + protected int $to; + + public function __construct(int $from, ?int $to = null, ?Worksheet $worksheet = null) + { + $this->validateFromTo($from, $to ?? $from); + $this->worksheet = $worksheet; + } + + public function __destruct() + { + $this->worksheet = null; + } + + /** @param array{int, int} $array */ + public static function fromArray(array $array, ?Worksheet $worksheet = null): self + { + [$from, $to] = $array; + + return new self($from, $to, $worksheet); + } + + private function validateFromTo(int $from, int $to): void + { + // Identify actual top and bottom values (in case we've been given bottom and top) + $this->from = min($from, $to); + $this->to = max($from, $to); + } + + public function from(): int + { + return $this->from; + } + + public function to(): int + { + return $this->to; + } + + public function rowCount(): int + { + return $this->to - $this->from + 1; + } + + public function shiftRight(int $offset = 1): self + { + $newFrom = $this->from + $offset; + $newFrom = ($newFrom < 1) ? 1 : $newFrom; + + $newTo = $this->to + $offset; + $newTo = ($newTo < 1) ? 1 : $newTo; + + return new self($newFrom, $newTo, $this->worksheet); + } + + public function shiftLeft(int $offset = 1): self + { + return $this->shiftRight(0 - $offset); + } + + public function toCellRange(): CellRange + { + return new CellRange( + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString('A'), $this->from, $this->worksheet), + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(AddressRange::MAX_COLUMN), $this->to) + ); + } + + public function __toString(): string + { + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$this->from}:{$this->to}"; + } + + return "{$this->from}:{$this->to}"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/StringValueBinder.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/StringValueBinder.php new file mode 100644 index 0000000..1509a9c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -0,0 +1,135 @@ +setIgnoredErrors = $setIgnoredErrors; + + return $this; + } + + public function setNullConversion(bool $suppressConversion = false): self + { + $this->convertNull = $suppressConversion; + + return $this; + } + + public function setBooleanConversion(bool $suppressConversion = false): self + { + $this->convertBoolean = $suppressConversion; + + return $this; + } + + public function getBooleanConversion(): bool + { + return $this->convertBoolean; + } + + public function setNumericConversion(bool $suppressConversion = false): self + { + $this->convertNumeric = $suppressConversion; + + return $this; + } + + public function setFormulaConversion(bool $suppressConversion = false): self + { + $this->convertFormula = $suppressConversion; + + return $this; + } + + public function setConversionForAllValueTypes(bool $suppressConversion = false): self + { + $this->convertNull = $suppressConversion; + $this->convertBoolean = $suppressConversion; + $this->convertNumeric = $suppressConversion; + $this->convertFormula = $suppressConversion; + + return $this; + } + + /** + * Bind value to a cell. + * + * @param Cell $cell Cell to bind value to + * @param mixed $value Value to bind in cell + */ + public function bindValue(Cell $cell, mixed $value): bool + { + if (is_object($value)) { + return $this->bindObjectValue($cell, $value); + } + if ($value !== null && !is_scalar($value)) { + throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value)); + } + + // sanitize UTF-8 strings + if (is_string($value)) { + $value = StringHelper::sanitizeUTF8($value); + } + + $ignoredErrors = false; + if ($value === null && $this->convertNull === false) { + $cell->setValueExplicit($value, DataType::TYPE_NULL); + } elseif (is_bool($value) && $this->convertBoolean === false) { + $cell->setValueExplicit($value, DataType::TYPE_BOOL); + } elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) { + $cell->setValueExplicit($value, DataType::TYPE_NUMERIC); + } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) { + $cell->setValueExplicit($value, DataType::TYPE_FORMULA); + } else { + $ignoredErrors = is_numeric($value); + $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); + } + if ($this->setIgnoredErrors) { + $cell->getIgnoredErrors()->setNumberStoredAsText($ignoredErrors); + } + + return true; + } + + protected function bindObjectValue(Cell $cell, object $value): bool + { + // Handle any objects that might be injected + $ignoredErrors = false; + if ($value instanceof DateTimeInterface) { + $value = $value->format('Y-m-d H:i:s'); + $cell->setValueExplicit($value, DataType::TYPE_STRING); + } elseif ($value instanceof RichText) { + $cell->setValueExplicit($value, DataType::TYPE_INLINE); + $ignoredErrors = is_numeric($value->getPlainText()); + } elseif ($value instanceof Stringable) { + $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); + $ignoredErrors = is_numeric((string) $value); + } else { + throw new SpreadsheetException('Unable to bind unstringable object of type ' . get_class($value)); + } + if ($this->setIgnoredErrors) { + $cell->getIgnoredErrors()->setNumberStoredAsText($ignoredErrors); + } + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/CellReferenceHelper.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/CellReferenceHelper.php new file mode 100644 index 0000000..a8d20f6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/CellReferenceHelper.php @@ -0,0 +1,178 @@ +beforeColumnAbsolute = $beforeCellAddress[0] === '$'; + $this->beforeRowAbsolute = strpos($beforeCellAddress, '$', 1) !== false; + $this->beforeCellAddress = str_replace('$', '', $beforeCellAddress); + $this->numberOfColumns = $numberOfColumns; + $this->numberOfRows = $numberOfRows; + + // Get coordinate of $beforeCellAddress + [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); + $this->beforeColumnString = $beforeColumn; + $this->beforeColumn = (int) Coordinate::columnIndexFromString($beforeColumn); + $this->beforeRow = (int) $beforeRow; + } + + public function beforeCellAddress(): string + { + return $this->beforeCellAddress; + } + + public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool + { + return $this->beforeCellAddress !== $beforeCellAddress + || $this->numberOfColumns !== $numberOfColumns + || $this->numberOfRows !== $numberOfRows; + } + + public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null): string + { + if (Coordinate::coordinateIsRange($cellReference)) { + throw new Exception('Only single cell references may be passed to this method.'); + } + + // Get coordinate of $cellReference + [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); + $newColumnIndex = Coordinate::columnIndexFromString(str_replace('$', '', $newColumn)); + $newRowIndex = (int) str_replace('$', '', $newRow); + + $absoluteColumn = $newColumn[0] === '$' ? '$' : ''; + $absoluteRow = $newRow[0] === '$' ? '$' : ''; + // Verify which parts should be updated + if ($onlyAbsoluteReferences === true) { + $updateColumn = (($absoluteColumn === '$') && $newColumnIndex >= $this->beforeColumn); + $updateRow = (($absoluteRow === '$') && $newRowIndex >= $this->beforeRow); + } elseif ($includeAbsoluteReferences === false) { + $updateColumn = (($absoluteColumn !== '$') && $newColumnIndex >= $this->beforeColumn); + $updateRow = (($absoluteRow !== '$') && $newRowIndex >= $this->beforeRow); + } else { + $newColumnIndex = $this->computeNewColumnIndex($newColumnIndex, $topLeft); + $newColumn = $absoluteColumn . Coordinate::stringFromColumnIndex($newColumnIndex); + $updateColumn = false; + + $newRowIndex = $this->computeNewRowIndex($newRowIndex, $topLeft); + $newRow = $absoluteRow . $newRowIndex; + $updateRow = false; + } + + // Create new column reference + if ($updateColumn) { + $newColumn = $this->updateColumnReference($newColumnIndex, $absoluteColumn); + } + + // Create new row reference + if ($updateRow) { + $newRow = $this->updateRowReference($newRowIndex, $absoluteRow); + } + + // Return new reference + return "{$newColumn}{$newRow}"; + } + + public function computeNewColumnIndex(int $newColumnIndex, ?bool $topLeft): int + { + // A special case is removing the left/top or bottom/right edge of a range + // $topLeft is null if we aren't adjusting a range at all. + if ( + $topLeft !== null + && $this->numberOfColumns < 0 + && $newColumnIndex >= $this->beforeColumn + $this->numberOfColumns + && $newColumnIndex <= $this->beforeColumn - 1 + ) { + if ($topLeft) { + $newColumnIndex = $this->beforeColumn + $this->numberOfColumns; + } else { + $newColumnIndex = $this->beforeColumn + $this->numberOfColumns - 1; + } + } elseif ($newColumnIndex >= $this->beforeColumn) { + // Create new column reference + $newColumnIndex += $this->numberOfColumns; + } + + return $newColumnIndex; + } + + public function computeNewRowIndex(int $newRowIndex, ?bool $topLeft): int + { + // A special case is removing the left/top or bottom/right edge of a range + // $topLeft is null if we aren't adjusting a range at all. + if ( + $topLeft !== null + && $this->numberOfRows < 0 + && $newRowIndex >= $this->beforeRow + $this->numberOfRows + && $newRowIndex <= $this->beforeRow - 1 + ) { + if ($topLeft) { + $newRowIndex = $this->beforeRow + $this->numberOfRows; + } else { + $newRowIndex = $this->beforeRow + $this->numberOfRows - 1; + } + } elseif ($newRowIndex >= $this->beforeRow) { + $newRowIndex = $newRowIndex + $this->numberOfRows; + } + + return $newRowIndex; + } + + public function cellAddressInDeleteRange(string $cellAddress): bool + { + [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); + $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); + // Is cell within the range of rows/columns if we're deleting + if ( + $this->numberOfRows < 0 + && ($cellRow >= ($this->beforeRow + $this->numberOfRows)) + && ($cellRow < $this->beforeRow) + ) { + return true; + } elseif ( + $this->numberOfColumns < 0 + && ($cellColumnIndex >= ($this->beforeColumn + $this->numberOfColumns)) + && ($cellColumnIndex < $this->beforeColumn) + ) { + return true; + } + + return false; + } + + protected function updateColumnReference(int $newColumnIndex, string $absoluteColumn): string + { + $newColumn = Coordinate::stringFromColumnIndex(min($newColumnIndex + $this->numberOfColumns, AddressRange::MAX_COLUMN_INT)); + + return "{$absoluteColumn}{$newColumn}"; + } + + protected function updateRowReference(int $newRowIndex, string $absoluteRow): string + { + $newRow = $newRowIndex + $this->numberOfRows; + $newRow = ($newRow > AddressRange::MAX_ROW) ? AddressRange::MAX_ROW : $newRow; + + return "{$absoluteRow}{$newRow}"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Axis.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Axis.php new file mode 100644 index 0000000..dc1fca4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Axis.php @@ -0,0 +1,344 @@ +fillColor = new ChartColor(); + } + + /** + * Chart Major Gridlines as. + */ + private ?GridLines $majorGridlines = null; + + /** + * Chart Minor Gridlines as. + */ + private ?GridLines $minorGridlines = null; + + /** + * Axis Number. + * + * @var array{format: string, source_linked: int, numeric: ?bool} + */ + private array $axisNumber = [ + 'format' => self::FORMAT_CODE_GENERAL, + 'source_linked' => 1, + 'numeric' => null, + ]; + + private string $axisType = ''; + + private ?AxisText $axisText = null; + + private ?Title $dispUnitsTitle = null; + + /** + * Axis Options. + * + * @var array + */ + private array $axisOptions = [ + 'minimum' => null, + 'maximum' => null, + 'major_unit' => null, + 'minor_unit' => null, + 'orientation' => self::ORIENTATION_NORMAL, + 'minor_tick_mark' => self::TICK_MARK_NONE, + 'major_tick_mark' => self::TICK_MARK_NONE, + 'axis_labels' => self::AXIS_LABELS_NEXT_TO, + 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, + 'horizontal_crosses_value' => null, + 'textRotation' => null, + 'hidden' => null, + 'majorTimeUnit' => self::TIME_UNIT_YEARS, + 'minorTimeUnit' => self::TIME_UNIT_MONTHS, + 'baseTimeUnit' => self::TIME_UNIT_DAYS, + 'logBase' => null, + 'dispUnitsBuiltIn' => null, + ]; + public const DISP_UNITS_HUNDREDS = 'hundreds'; + public const DISP_UNITS_THOUSANDS = 'thousands'; + public const DISP_UNITS_TEN_THOUSANDS = 'tenThousands'; + public const DISP_UNITS_HUNDRED_THOUSANDS = 'hundredThousands'; + public const DISP_UNITS_MILLIONS = 'millions'; + public const DISP_UNITS_TEN_MILLIONS = 'tenMillions'; + public const DISP_UNITS_HUNDRED_MILLIONS = 'hundredMillions'; + public const DISP_UNITS_BILLIONS = 'billions'; + public const DISP_UNITS_TRILLIONS = 'trillions'; + public const TRILLION_INDEX = (PHP_INT_SIZE > 4) ? 1000000000000 : '1000000000000'; + public const DISP_UNITS_BUILTIN_INT = [ + 100 => self::DISP_UNITS_HUNDREDS, + 1000 => self::DISP_UNITS_THOUSANDS, + 10000 => self::DISP_UNITS_TEN_THOUSANDS, + 100000 => self::DISP_UNITS_HUNDRED_THOUSANDS, + 1000000 => self::DISP_UNITS_MILLIONS, + 10000000 => self::DISP_UNITS_TEN_MILLIONS, + 100000000 => self::DISP_UNITS_HUNDRED_MILLIONS, + 1000000000 => self::DISP_UNITS_BILLIONS, + self::TRILLION_INDEX => self::DISP_UNITS_TRILLIONS, // overflow for 32-bit + ]; + + /** + * Fill Properties. + */ + private ChartColor $fillColor; + + private const NUMERIC_FORMAT = [ + Properties::FORMAT_CODE_NUMBER, + Properties::FORMAT_CODE_DATE, + Properties::FORMAT_CODE_DATE_ISO8601, + ]; + + private bool $noFill = false; + + /** + * Get Series Data Type. + */ + public function setAxisNumberProperties(string $format_code, ?bool $numeric = null, int $sourceLinked = 0): void + { + $format = $format_code; + $this->axisNumber['format'] = $format; + $this->axisNumber['source_linked'] = $sourceLinked; + if (is_bool($numeric)) { + $this->axisNumber['numeric'] = $numeric; + } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { + $this->axisNumber['numeric'] = true; + } + } + + /** + * Get Axis Number Format Data Type. + */ + public function getAxisNumberFormat(): string + { + return $this->axisNumber['format']; + } + + /** + * Get Axis Number Source Linked. + */ + public function getAxisNumberSourceLinked(): string + { + return (string) $this->axisNumber['source_linked']; + } + + public function getAxisIsNumericFormat(): bool + { + return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; + } + + public function setAxisOption(string $key, null|float|int|string $value): void + { + if ($value !== null && $value !== '') { + $this->axisOptions[$key] = (string) $value; + } + } + + /** + * Set Axis Options Properties. + */ + public function setAxisOptionsProperties( + string $axisLabels, + ?string $horizontalCrossesValue = null, + ?string $horizontalCrosses = null, + ?string $axisOrientation = null, + ?string $majorTmt = null, + ?string $minorTmt = null, + null|float|int|string $minimum = null, + null|float|int|string $maximum = null, + null|float|int|string $majorUnit = null, + null|float|int|string $minorUnit = null, + null|float|int|string $textRotation = null, + ?string $hidden = null, + ?string $baseTimeUnit = null, + ?string $majorTimeUnit = null, + ?string $minorTimeUnit = null, + null|float|int|string $logBase = null, + ?string $dispUnitsBuiltIn = null + ): void { + $this->axisOptions['axis_labels'] = $axisLabels; + $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); + $this->setAxisOption('horizontal_crosses', $horizontalCrosses); + $this->setAxisOption('orientation', $axisOrientation); + $this->setAxisOption('major_tick_mark', $majorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minimum', $minimum); + $this->setAxisOption('maximum', $maximum); + $this->setAxisOption('major_unit', $majorUnit); + $this->setAxisOption('minor_unit', $minorUnit); + $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); + $this->setAxisOption('baseTimeUnit', $baseTimeUnit); + $this->setAxisOption('majorTimeUnit', $majorTimeUnit); + $this->setAxisOption('minorTimeUnit', $minorTimeUnit); + $this->setAxisOption('logBase', $logBase); + $this->setAxisOption('dispUnitsBuiltIn', $dispUnitsBuiltIn); + } + + /** + * Get Axis Options Property. + */ + public function getAxisOptionsProperty(string $property): ?string + { + if ($property === 'textRotation') { + if ($this->axisText !== null) { + if ($this->axisText->getRotation() !== null) { + return (string) $this->axisText->getRotation(); + } + } + } + + return $this->axisOptions[$property]; + } + + /** + * Set Axis Orientation Property. + */ + public function setAxisOrientation(string $orientation): void + { + $this->axisOptions['orientation'] = (string) $orientation; + } + + public function getAxisType(): string + { + return $this->axisType; + } + + public function setAxisType(string $type): self + { + if ($type === self::AXIS_TYPE_CATEGORY || $type === self::AXIS_TYPE_VALUE || $type === self::AXIS_TYPE_DATE) { + $this->axisType = $type; + } else { + $this->axisType = ''; + } + + return $this; + } + + /** + * Set Fill Property. + */ + public function setFillParameters(?string $color, ?int $alpha = null, ?string $AlphaType = ChartColor::EXCEL_COLOR_TYPE_RGB): void + { + $this->fillColor->setColorProperties($color, $alpha, $AlphaType); + } + + /** + * Get Fill Property. + */ + public function getFillProperty(string $property): string + { + return (string) $this->fillColor->getColorProperty($property); + } + + public function getFillColorObject(): ChartColor + { + return $this->fillColor; + } + + private string $crossBetween = ''; // 'between' or 'midCat' might be better + + public function setCrossBetween(string $crossBetween): self + { + $this->crossBetween = $crossBetween; + + return $this; + } + + public function getCrossBetween(): string + { + return $this->crossBetween; + } + + public function getMajorGridlines(): ?GridLines + { + return $this->majorGridlines; + } + + public function getMinorGridlines(): ?GridLines + { + return $this->minorGridlines; + } + + public function setMajorGridlines(?GridLines $gridlines): self + { + $this->majorGridlines = $gridlines; + + return $this; + } + + public function setMinorGridlines(?GridLines $gridlines): self + { + $this->minorGridlines = $gridlines; + + return $this; + } + + public function getAxisText(): ?AxisText + { + return $this->axisText; + } + + public function setAxisText(?AxisText $axisText): self + { + $this->axisText = $axisText; + + return $this; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setDispUnitsTitle(?Title $dispUnitsTitle): self + { + $this->dispUnitsTitle = $dispUnitsTitle; + + return $this; + } + + public function getDispUnitsTitle(): ?Title + { + return $this->dispUnitsTitle; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + parent::__clone(); + $this->majorGridlines = ($this->majorGridlines === null) ? null : clone $this->majorGridlines; + $this->majorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines; + $this->axisText = ($this->axisText === null) ? null : clone $this->axisText; + $this->dispUnitsTitle = ($this->dispUnitsTitle === null) ? null : clone $this->dispUnitsTitle; + $this->fillColor = clone $this->fillColor; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/AxisText.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/AxisText.php new file mode 100644 index 0000000..09d6b2a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/AxisText.php @@ -0,0 +1,63 @@ +font = new Font(); + $this->font->setSize(null, true); + } + + public function setRotation(?int $rotation): self + { + $this->rotation = $rotation; + + return $this; + } + + public function getRotation(): ?int + { + return $this->rotation; + } + + public function getFillColorObject(): ChartColor + { + $fillColor = $this->font->getChartColor(); + if ($fillColor === null) { + $fillColor = new ChartColor(); + $this->font->setChartColorFromObject($fillColor); + } + + return $fillColor; + } + + public function getFont(): Font + { + return $this->font; + } + + public function setFont(Font $font): self + { + $this->font = $font; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + parent::__clone(); + $this->font = clone $this->font; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Chart.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Chart.php new file mode 100644 index 0000000..e750edc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Chart.php @@ -0,0 +1,785 @@ +name = $name; + $this->title = $title; + $this->legend = $legend; + $this->xAxisLabel = $xAxisLabel; + $this->yAxisLabel = $yAxisLabel; + $this->plotArea = $plotArea; + $this->plotVisibleOnly = $plotVisibleOnly; + $this->setDisplayBlanksAs($displayBlanksAs); + $this->xAxis = $xAxis ?? new Axis(); + $this->yAxis = $yAxis ?? new Axis(); + if ($majorGridlines !== null) { + $this->yAxis->setMajorGridlines($majorGridlines); + } + if ($minorGridlines !== null) { + $this->yAxis->setMinorGridlines($minorGridlines); + } + $this->fillColor = new ChartColor(); + $this->borderLines = new GridLines(); + } + + public function __destruct() + { + $this->worksheet = null; + } + + /** + * Get Name. + */ + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * Get Worksheet. + */ + public function getWorksheet(): ?Worksheet + { + return $this->worksheet; + } + + /** + * Set Worksheet. + * + * @return $this + */ + public function setWorksheet(?Worksheet $worksheet = null): static + { + $this->worksheet = $worksheet; + + return $this; + } + + public function getTitle(): ?Title + { + return $this->title; + } + + /** + * Set Title. + * + * @return $this + */ + public function setTitle(Title $title): static + { + $this->title = $title; + + return $this; + } + + public function getLegend(): ?Legend + { + return $this->legend; + } + + /** + * Set Legend. + * + * @return $this + */ + public function setLegend(Legend $legend): static + { + $this->legend = $legend; + + return $this; + } + + public function getXAxisLabel(): ?Title + { + return $this->xAxisLabel; + } + + /** + * Set X-Axis Label. + * + * @return $this + */ + public function setXAxisLabel(Title $label): static + { + $this->xAxisLabel = $label; + + return $this; + } + + public function getYAxisLabel(): ?Title + { + return $this->yAxisLabel; + } + + /** + * Set Y-Axis Label. + * + * @return $this + */ + public function setYAxisLabel(Title $label): static + { + $this->yAxisLabel = $label; + + return $this; + } + + public function getPlotArea(): ?PlotArea + { + return $this->plotArea; + } + + public function getPlotAreaOrThrow(): PlotArea + { + $plotArea = $this->getPlotArea(); + if ($plotArea !== null) { + return $plotArea; + } + + throw new Exception('Chart has no PlotArea'); + } + + /** + * Set Plot Area. + */ + public function setPlotArea(PlotArea $plotArea): self + { + $this->plotArea = $plotArea; + + return $this; + } + + /** + * Get Plot Visible Only. + */ + public function getPlotVisibleOnly(): bool + { + return $this->plotVisibleOnly; + } + + /** + * Set Plot Visible Only. + * + * @return $this + */ + public function setPlotVisibleOnly(bool $plotVisibleOnly): static + { + $this->plotVisibleOnly = $plotVisibleOnly; + + return $this; + } + + /** + * Get Display Blanks as. + */ + public function getDisplayBlanksAs(): string + { + return $this->displayBlanksAs; + } + + /** + * Set Display Blanks as. + * + * @return $this + */ + public function setDisplayBlanksAs(string $displayBlanksAs): static + { + $displayBlanksAs = strtolower($displayBlanksAs); + $this->displayBlanksAs = in_array($displayBlanksAs, DataSeries::VALID_EMPTY_AS, true) ? $displayBlanksAs : DataSeries::DEFAULT_EMPTY_AS; + + return $this; + } + + public function getChartAxisY(): Axis + { + return $this->yAxis; + } + + /** + * Set yAxis. + */ + public function setChartAxisY(?Axis $axis): self + { + $this->yAxis = $axis ?? new Axis(); + + return $this; + } + + public function getChartAxisX(): Axis + { + return $this->xAxis; + } + + /** + * Set xAxis. + */ + public function setChartAxisX(?Axis $axis): self + { + $this->xAxis = $axis ?? new Axis(); + + return $this; + } + + /** + * Set the Top Left position for the chart. + * + * @return $this + */ + public function setTopLeftPosition(string $cellAddress, ?int $xOffset = null, ?int $yOffset = null): static + { + $this->topLeftCellRef = $cellAddress; + if ($xOffset !== null) { + $this->setTopLeftXOffset($xOffset); + } + if ($yOffset !== null) { + $this->setTopLeftYOffset($yOffset); + } + + return $this; + } + + /** + * Get the top left position of the chart. + * + * Returns ['cell' => string cell address, 'xOffset' => int, 'yOffset' => int]. + * + * @return array{cell: string, xOffset: int, yOffset: int} an associative array containing the cell address, X-Offset and Y-Offset from the top left of that cell + */ + public function getTopLeftPosition(): array + { + return [ + 'cell' => $this->topLeftCellRef, + 'xOffset' => $this->topLeftXOffset, + 'yOffset' => $this->topLeftYOffset, + ]; + } + + /** + * Get the cell address where the top left of the chart is fixed. + */ + public function getTopLeftCell(): string + { + return $this->topLeftCellRef; + } + + /** + * Set the Top Left cell position for the chart. + * + * @return $this + */ + public function setTopLeftCell(string $cellAddress): static + { + $this->topLeftCellRef = $cellAddress; + + return $this; + } + + /** + * Set the offset position within the Top Left cell for the chart. + * + * @return $this + */ + public function setTopLeftOffset(?int $xOffset, ?int $yOffset): static + { + if ($xOffset !== null) { + $this->setTopLeftXOffset($xOffset); + } + + if ($yOffset !== null) { + $this->setTopLeftYOffset($yOffset); + } + + return $this; + } + + /** + * Get the offset position within the Top Left cell for the chart. + * + * @return int[] + */ + public function getTopLeftOffset(): array + { + return [ + 'X' => $this->topLeftXOffset, + 'Y' => $this->topLeftYOffset, + ]; + } + + /** + * @return $this + */ + public function setTopLeftXOffset(int $xOffset): static + { + $this->topLeftXOffset = $xOffset; + + return $this; + } + + public function getTopLeftXOffset(): int + { + return $this->topLeftXOffset; + } + + /** + * @return $this + */ + public function setTopLeftYOffset(int $yOffset): static + { + $this->topLeftYOffset = $yOffset; + + return $this; + } + + public function getTopLeftYOffset(): int + { + return $this->topLeftYOffset; + } + + /** + * Set the Bottom Right position of the chart. + * + * @return $this + */ + public function setBottomRightPosition(string $cellAddress = '', ?int $xOffset = null, ?int $yOffset = null): static + { + $this->bottomRightCellRef = $cellAddress; + if ($xOffset !== null) { + $this->setBottomRightXOffset($xOffset); + } + if ($yOffset !== null) { + $this->setBottomRightYOffset($yOffset); + } + + return $this; + } + + /** + * Get the bottom right position of the chart. + * + * @return array{cell: string, xOffset: int, yOffset:int} an associative array containing the cell address, X-Offset and Y-Offset from the top left of that cell + */ + public function getBottomRightPosition(): array + { + return [ + 'cell' => $this->bottomRightCellRef, + 'xOffset' => $this->bottomRightXOffset, + 'yOffset' => $this->bottomRightYOffset, + ]; + } + + /** + * Set the Bottom Right cell for the chart. + * + * @return $this + */ + public function setBottomRightCell(string $cellAddress = ''): static + { + $this->bottomRightCellRef = $cellAddress; + + return $this; + } + + /** + * Get the cell address where the bottom right of the chart is fixed. + */ + public function getBottomRightCell(): string + { + return $this->bottomRightCellRef; + } + + /** + * Set the offset position within the Bottom Right cell for the chart. + * + * @return $this + */ + public function setBottomRightOffset(?int $xOffset, ?int $yOffset): static + { + if ($xOffset !== null) { + $this->setBottomRightXOffset($xOffset); + } + + if ($yOffset !== null) { + $this->setBottomRightYOffset($yOffset); + } + + return $this; + } + + /** + * Get the offset position within the Bottom Right cell for the chart. + * + * @return int[] + */ + public function getBottomRightOffset(): array + { + return [ + 'X' => $this->bottomRightXOffset, + 'Y' => $this->bottomRightYOffset, + ]; + } + + /** + * @return $this + */ + public function setBottomRightXOffset(int $xOffset): static + { + $this->bottomRightXOffset = $xOffset; + + return $this; + } + + public function getBottomRightXOffset(): int + { + return $this->bottomRightXOffset; + } + + /** + * @return $this + */ + public function setBottomRightYOffset(int $yOffset): static + { + $this->bottomRightYOffset = $yOffset; + + return $this; + } + + public function getBottomRightYOffset(): int + { + return $this->bottomRightYOffset; + } + + public function refresh(): void + { + if ($this->worksheet !== null && $this->plotArea !== null) { + $this->plotArea->refresh($this->worksheet); + } + } + + /** + * Render the chart to given file (or stream). + * + * @param ?string $outputDestination Name of the file render to + * + * @return bool true on success + */ + public function render(?string $outputDestination = null): bool + { + if ($outputDestination == 'php://output') { + $outputDestination = null; + } + + $libraryName = Settings::getChartRenderer(); + if ($libraryName === null) { + return false; + } + + // Ensure that data series values are up-to-date before we render + $this->refresh(); + + $renderer = new $libraryName($this); + + return $renderer->render($outputDestination); + } + + public function getRotX(): ?int + { + return $this->rotX; + } + + public function setRotX(?int $rotX): self + { + $this->rotX = $rotX; + + return $this; + } + + public function getRotY(): ?int + { + return $this->rotY; + } + + public function setRotY(?int $rotY): self + { + $this->rotY = $rotY; + + return $this; + } + + public function getRAngAx(): ?int + { + return $this->rAngAx; + } + + public function setRAngAx(?int $rAngAx): self + { + $this->rAngAx = $rAngAx; + + return $this; + } + + public function getPerspective(): ?int + { + return $this->perspective; + } + + public function setPerspective(?int $perspective): self + { + $this->perspective = $perspective; + + return $this; + } + + public function getOneCellAnchor(): bool + { + return $this->oneCellAnchor; + } + + public function setOneCellAnchor(bool $oneCellAnchor): self + { + $this->oneCellAnchor = $oneCellAnchor; + + return $this; + } + + public function getAutoTitleDeleted(): bool + { + return $this->autoTitleDeleted; + } + + public function setAutoTitleDeleted(bool $autoTitleDeleted): self + { + $this->autoTitleDeleted = $autoTitleDeleted; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoBorder(): bool + { + return $this->noBorder; + } + + public function setNoBorder(bool $noBorder): self + { + $this->noBorder = $noBorder; + + return $this; + } + + public function getRoundedCorners(): bool + { + return $this->roundedCorners; + } + + public function setRoundedCorners(?bool $roundedCorners): self + { + if ($roundedCorners !== null) { + $this->roundedCorners = $roundedCorners; + } + + return $this; + } + + public function getBorderLines(): GridLines + { + return $this->borderLines; + } + + public function setBorderLines(GridLines $borderLines): self + { + $this->borderLines = $borderLines; + + return $this; + } + + public function getFillColor(): ChartColor + { + return $this->fillColor; + } + + public function setRenderedWidth(?float $width): self + { + $this->renderedWidth = $width; + + return $this; + } + + public function getRenderedWidth(): ?float + { + return $this->renderedWidth; + } + + public function setRenderedHeight(?float $height): self + { + $this->renderedHeight = $height; + + return $this; + } + + public function getRenderedHeight(): ?float + { + return $this->renderedHeight; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->worksheet = null; + $this->title = ($this->title === null) ? null : clone $this->title; + $this->legend = ($this->legend === null) ? null : clone $this->legend; + $this->xAxisLabel = ($this->xAxisLabel === null) ? null : clone $this->xAxisLabel; + $this->yAxisLabel = ($this->yAxisLabel === null) ? null : clone $this->yAxisLabel; + $this->plotArea = ($this->plotArea === null) ? null : clone $this->plotArea; + $this->xAxis = clone $this->xAxis; + $this->yAxis = clone $this->yAxis; + $this->borderLines = clone $this->borderLines; + $this->fillColor = clone $this->fillColor; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/ChartColor.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/ChartColor.php new file mode 100644 index 0000000..6ac4b5a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/ChartColor.php @@ -0,0 +1,161 @@ +setColorPropertiesArray($value); + } else { + $this->setColorProperties($value, $alpha, $type, $brightness); + } + } + + public function getValue(): string + { + return $this->value; + } + + public function setValue(string $value): self + { + $this->value = $value; + + return $this; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getAlpha(): ?int + { + return $this->alpha; + } + + public function setAlpha(?int $alpha): self + { + $this->alpha = $alpha; + + return $this; + } + + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + + public function setColorProperties(?string $color, null|float|int|string $alpha = null, ?string $type = null, null|float|int|string $brightness = null): self + { + if (empty($type) && !empty($color)) { + if (str_starts_with($color, '*')) { + $type = 'schemeClr'; + $color = substr($color, 1); + } elseif (str_starts_with($color, '/')) { + $type = 'prstClr'; + $color = substr($color, 1); + } elseif (preg_match('/^[0-9A-Fa-f]{6}$/', $color) === 1) { + $type = 'srgbClr'; + } + } + if ($color !== null) { + $this->setValue("$color"); + } + if ($type !== null) { + $this->setType($type); + } + if ($alpha === null) { + $this->setAlpha(null); + } elseif (is_numeric($alpha)) { + $this->setAlpha((int) $alpha); + } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } + + return $this; + } + + /** @param array{value: ?string, alpha: null|int|string, brightness?: null|int|string, type: ?string} $color */ + public function setColorPropertiesArray(array $color): self + { + return $this->setColorProperties( + $color['value'] ?? '', + $color['alpha'] ?? null, + $color['type'] ?? null, + $color['brightness'] ?? null + ); + } + + public function isUsable(): bool + { + return $this->type !== '' && $this->value !== ''; + } + + /** + * Get Color Property. + */ + public function getColorProperty(string $propertyName): null|int|string + { + $retVal = null; + if ($propertyName === 'value') { + $retVal = $this->value; + } elseif ($propertyName === 'type') { + $retVal = $this->type; + } elseif ($propertyName === 'alpha') { + $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; + } + + return $retVal; + } + + public static function alphaToXml(int $alpha): string + { + return (string) (100 - $alpha) . '000'; + } + + public static function alphaFromXml(float|int|string $alpha): int + { + return 100 - ((int) $alpha / 1000); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/DataSeries.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/DataSeries.php new file mode 100644 index 0000000..06f1c3c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/DataSeries.php @@ -0,0 +1,408 @@ +plotType = $plotType; + $this->plotGrouping = $plotGrouping; + $this->plotOrder = $plotOrder; + $keys = array_keys($plotValues); + $this->plotValues = $plotValues; + if (!isset($plotLabel[$keys[0]])) { + $plotLabel[$keys[0]] = new DataSeriesValues(); + } + $this->plotLabel = $plotLabel; + + if (!isset($plotCategory[$keys[0]])) { + $plotCategory[$keys[0]] = new DataSeriesValues(); + } + $this->plotCategory = $plotCategory; + + $this->smoothLine = (bool) $smoothLine; + $this->plotStyle = $plotStyle; + + if ($plotDirection === null) { + $plotDirection = self::DIRECTION_COL; + } + $this->plotDirection = $plotDirection; + } + + /** + * Get Plot Type. + */ + public function getPlotType(): ?string + { + return $this->plotType; + } + + /** + * Set Plot Type. + * + * @return $this + */ + public function setPlotType(string $plotType): static + { + $this->plotType = $plotType; + + return $this; + } + + /** + * Get Plot Grouping Type. + */ + public function getPlotGrouping(): ?string + { + return $this->plotGrouping; + } + + /** + * Set Plot Grouping Type. + * + * @return $this + */ + public function setPlotGrouping(string $groupingType): static + { + $this->plotGrouping = $groupingType; + + return $this; + } + + /** + * Get Plot Direction. + */ + public function getPlotDirection(): string + { + return $this->plotDirection; + } + + /** + * Set Plot Direction. + * + * @return $this + */ + public function setPlotDirection(string $plotDirection): static + { + $this->plotDirection = $plotDirection; + + return $this; + } + + /** + * Get Plot Order. + * + * @return int[] + */ + public function getPlotOrder(): array + { + return $this->plotOrder; + } + + /** + * Get Plot Labels. + * + * @return DataSeriesValues[] + */ + public function getPlotLabels(): array + { + return $this->plotLabel; + } + + /** + * Get Plot Label by Index. + * + * @return DataSeriesValues|false + */ + public function getPlotLabelByIndex(int $index): bool|DataSeriesValues + { + $keys = array_keys($this->plotLabel); + if (in_array($index, $keys)) { + return $this->plotLabel[$index]; + } + + return false; + } + + /** + * Get Plot Categories. + * + * @return DataSeriesValues[] + */ + public function getPlotCategories(): array + { + return $this->plotCategory; + } + + /** + * Get Plot Category by Index. + * + * @return DataSeriesValues|false + */ + public function getPlotCategoryByIndex(int $index): bool|DataSeriesValues + { + $keys = array_keys($this->plotCategory); + if (in_array($index, $keys)) { + return $this->plotCategory[$index]; + } elseif (isset($keys[$index])) { + return $this->plotCategory[$keys[$index]]; + } + + return false; + } + + /** + * Get Plot Style. + */ + public function getPlotStyle(): ?string + { + return $this->plotStyle; + } + + /** + * Set Plot Style. + * + * @return $this + */ + public function setPlotStyle(?string $plotStyle): static + { + $this->plotStyle = $plotStyle; + + return $this; + } + + /** + * Get Plot Values. + * + * @return DataSeriesValues[] + */ + public function getPlotValues(): array + { + return $this->plotValues; + } + + /** + * Get Plot Values by Index. + * + * @return DataSeriesValues|false + */ + public function getPlotValuesByIndex(int $index): bool|DataSeriesValues + { + $keys = array_keys($this->plotValues); + if (in_array($index, $keys)) { + return $this->plotValues[$index]; + } + + return false; + } + + /** + * Get Plot Bubble Sizes. + * + * @return DataSeriesValues[] + */ + public function getPlotBubbleSizes(): array + { + return $this->plotBubbleSizes; + } + + /** + * Set Plot Bubble Sizes. + * + * @param DataSeriesValues[] $plotBubbleSizes + */ + public function setPlotBubbleSizes(array $plotBubbleSizes): self + { + $this->plotBubbleSizes = $plotBubbleSizes; + + return $this; + } + + /** + * Get Number of Plot Series. + */ + public function getPlotSeriesCount(): int + { + return count($this->plotValues); + } + + /** + * Get Smooth Line. + */ + public function getSmoothLine(): bool + { + return $this->smoothLine; + } + + /** + * Set Smooth Line. + * + * @return $this + */ + public function setSmoothLine(bool $smoothLine): static + { + $this->smoothLine = $smoothLine; + + return $this; + } + + public function refresh(Worksheet $worksheet): void + { + foreach ($this->plotValues as $plotValues) { + $plotValues->refresh($worksheet, true); + } + foreach ($this->plotLabel as $plotValues) { + $plotValues->refresh($worksheet, true); + } + foreach ($this->plotCategory as $plotValues) { + $plotValues->refresh($worksheet, false); + } + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $plotLabels = $this->plotLabel; + $this->plotLabel = []; + foreach ($plotLabels as $plotLabel) { + $this->plotLabel[] = $plotLabel; + } + $plotCategories = $this->plotCategory; + $this->plotCategory = []; + foreach ($plotCategories as $plotCategory) { + $this->plotCategory[] = clone $plotCategory; + } + $plotValues = $this->plotValues; + $this->plotValues = []; + foreach ($plotValues as $plotValue) { + $this->plotValues[] = clone $plotValue; + } + $plotBubbleSizes = $this->plotBubbleSizes; + $this->plotBubbleSizes = []; + foreach ($plotBubbleSizes as $plotBubbleSize) { + $this->plotBubbleSizes[] = clone $plotBubbleSize; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/DataSeriesValues.php new file mode 100644 index 0000000..f378932 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -0,0 +1,581 @@ +markerFillColor = new ChartColor(); + $this->markerBorderColor = new ChartColor(); + $this->setDataType($dataType); + $this->dataSource = $dataSource; + $this->formatCode = $formatCode; + $this->pointCount = $pointCount; + $this->dataValues = $dataValues; + $this->pointMarker = $marker; + if ($fillColor !== null) { + $this->setFillColor($fillColor); + } + if (is_numeric($pointSize)) { + $this->pointSize = (int) $pointSize; + } + } + + /** + * Get Series Data Type. + */ + public function getDataType(): string + { + return $this->dataType; + } + + /** + * Set Series Data Type. + * + * @param string $dataType Datatype of this data series + * Typical values are: + * DataSeriesValues::DATASERIES_TYPE_STRING + * Normally used for axis point values + * DataSeriesValues::DATASERIES_TYPE_NUMBER + * Normally used for chart data values + * + * @return $this + */ + public function setDataType(string $dataType): static + { + if (!in_array($dataType, self::DATA_TYPE_VALUES)) { + throw new Exception('Invalid datatype for chart data series values'); + } + $this->dataType = $dataType; + + return $this; + } + + /** + * Get Series Data Source (formula). + */ + public function getDataSource(): ?string + { + return $this->dataSource; + } + + /** + * Set Series Data Source (formula). + * + * @return $this + */ + public function setDataSource(?string $dataSource): static + { + $this->dataSource = $dataSource; + + return $this; + } + + /** + * Get Point Marker. + */ + public function getPointMarker(): ?string + { + return $this->pointMarker; + } + + /** + * Set Point Marker. + * + * @return $this + */ + public function setPointMarker(string $marker): static + { + $this->pointMarker = $marker; + + return $this; + } + + public function getMarkerFillColor(): ChartColor + { + return $this->markerFillColor; + } + + public function getMarkerBorderColor(): ChartColor + { + return $this->markerBorderColor; + } + + /** + * Get Point Size. + */ + public function getPointSize(): int + { + return $this->pointSize; + } + + /** + * Set Point Size. + * + * @return $this + */ + public function setPointSize(int $size = 3): static + { + $this->pointSize = $size; + + return $this; + } + + /** + * Get Series Format Code. + */ + public function getFormatCode(): ?string + { + return $this->formatCode; + } + + /** + * Set Series Format Code. + * + * @return $this + */ + public function setFormatCode(string $formatCode): static + { + $this->formatCode = $formatCode; + + return $this; + } + + /** + * Get Series Point Count. + */ + public function getPointCount(): int + { + return $this->pointCount; + } + + /** + * Get fill color object. + * + * @return null|ChartColor|ChartColor[] + */ + public function getFillColorObject() + { + return $this->fillColor; + } + + private function stringToChartColor(string $fillString): ChartColor + { + $value = $type = ''; + if (str_starts_with($fillString, '*')) { + $type = 'schemeClr'; + $value = substr($fillString, 1); + } elseif (str_starts_with($fillString, '/')) { + $type = 'prstClr'; + $value = substr($fillString, 1); + } elseif ($fillString !== '') { + $type = 'srgbClr'; + $value = $fillString; + $this->validateColor($value); + } + + return new ChartColor($value, null, $type); + } + + private function chartColorToString(ChartColor $chartColor): string + { + $type = (string) $chartColor->getColorProperty('type'); + $value = (string) $chartColor->getColorProperty('value'); + if ($type === '' || $value === '') { + return ''; + } + if ($type === 'schemeClr') { + return "*$value"; + } + if ($type === 'prstClr') { + return "/$value"; + } + + return $value; + } + + /** + * Get fill color. + * + * @return string|string[] HEX color or array with HEX colors + */ + public function getFillColor(): string|array + { + if ($this->fillColor === null) { + return ''; + } + if (is_array($this->fillColor)) { + $array = []; + foreach ($this->fillColor as $chartColor) { + $array[] = $this->chartColorToString($chartColor); + } + + return $array; + } + + return $this->chartColorToString($this->fillColor); + } + + /** + * Set fill color for series. + * + * @param ChartColor|ChartColor[]|string|string[] $color HEX color or array with HEX colors + * + * @return $this + */ + public function setFillColor($color): static + { + if (is_array($color)) { + $this->fillColor = []; + foreach ($color as $fillString) { + if ($fillString instanceof ChartColor) { + $this->fillColor[] = $fillString; + } else { + $this->fillColor[] = $this->stringToChartColor($fillString); + } + } + } elseif ($color instanceof ChartColor) { + $this->fillColor = $color; + } else { + $this->fillColor = $this->stringToChartColor($color); + } + + return $this; + } + + /** + * Method for validating hex color. + * + * @param string $color value for color + */ + private function validateColor(string $color): void + { + if (!preg_match('/^[a-f0-9]{6}$/i', $color)) { + throw new Exception(sprintf('Invalid hex color for chart series (color: "%s")', $color)); + } + } + + /** + * Get line width for series. + */ + public function getLineWidth(): null|float|int + { + /** @var null|float|int */ + $temp = $this->lineStyleProperties['width']; + + return $temp; + } + + /** + * Set line width for the series. + * + * @return $this + */ + public function setLineWidth(null|float|int $width): static + { + $this->lineStyleProperties['width'] = $width; + + return $this; + } + + /** + * Identify if the Data Series is a multi-level or a simple series. + */ + public function isMultiLevelSeries(): ?bool + { + if (!empty($this->dataValues)) { + return is_array(array_values($this->dataValues)[0]); + } + + return null; + } + + /** + * Return the level count of a multi-level Data Series. + */ + public function multiLevelCount(): int + { + $levelCount = 0; + foreach (($this->dataValues ?? []) as $dataValueSet) { + /** @var mixed[] $dataValueSet */ + $levelCount = max($levelCount, count($dataValueSet)); + } + + return $levelCount; + } + + /** + * Get Series Data Values. + * + * @return null|mixed[] + */ + public function getDataValues(): ?array + { + return $this->dataValues; + } + + /** + * Get the first Series Data value. + */ + public function getDataValue(): mixed + { + if ($this->dataValues === null) { + return null; + } + $count = count($this->dataValues); + if ($count == 0) { + return null; + } elseif ($count == 1) { + return $this->dataValues[0]; + } + + return $this->dataValues; + } + + /** + * Set Series Data Values. + * + * @param mixed[] $dataValues + * + * @return $this + */ + public function setDataValues(array $dataValues): static + { + $this->dataValues = Functions::flattenArray($dataValues); + $this->pointCount = count($dataValues); + + return $this; + } + + public function refresh(Worksheet $worksheet, bool $flatten = true): void + { + if ($this->dataSource !== null) { + $calcEngine = Calculation::getInstance($worksheet->getParent()); + $newDataValues = Calculation::unwrapResult( + $calcEngine->_calculateFormulaValue( + '=' . $this->dataSource, + null, + $worksheet->getCell('A1') + ) + ); + if ($flatten) { + $this->dataValues = Functions::flattenArray($newDataValues); + foreach ($this->dataValues as &$dataValue) { + if (is_string($dataValue) && !empty($dataValue) && $dataValue[0] == '#') { + $dataValue = 0.0; + } + } + unset($dataValue); + } else { + [, $cellRange] = Worksheet::extractSheetTitle($this->dataSource, true); + $dimensions = Coordinate::rangeDimension(str_replace('$', '', $cellRange ?? '')); + if (($dimensions[0] == 1) || ($dimensions[1] == 1)) { + $this->dataValues = Functions::flattenArray($newDataValues); + } else { + /** @var array */ + $newDataValuesx = $newDataValues; + /** @var mixed[][] $newArray */ + $newArray = array_values(array_shift($newDataValuesx) ?? []); + foreach ($newArray as $i => $newDataSet) { + $newArray[$i] = [$newDataSet]; + } + + foreach ($newDataValuesx as $newDataSet) { + $i = 0; + foreach ($newDataSet as $newDataVal) { + array_unshift($newArray[$i++], $newDataVal); + } + } + $this->dataValues = $newArray; + } + } + $this->pointCount = count($this->dataValues ?? []); + } + } + + public function getScatterLines(): bool + { + return $this->scatterLines; + } + + public function setScatterLines(bool $scatterLines): self + { + $this->scatterLines = $scatterLines; + + return $this; + } + + public function getBubble3D(): bool + { + return $this->bubble3D; + } + + public function setBubble3D(bool $bubble3D): self + { + $this->bubble3D = $bubble3D; + + return $this; + } + + /** + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. + */ + private bool $smoothLine = false; + + /** + * Get Smooth Line. + */ + public function getSmoothLine(): bool + { + return $this->smoothLine; + } + + /** + * Set Smooth Line. + * + * @return $this + */ + public function setSmoothLine(bool $smoothLine): static + { + $this->smoothLine = $smoothLine; + + return $this; + } + + public function getLabelLayout(): ?Layout + { + return $this->labelLayout; + } + + public function setLabelLayout(?Layout $labelLayout): self + { + $this->labelLayout = $labelLayout; + + return $this; + } + + /** @param TrendLine[] $trendLines */ + public function setTrendLines(array $trendLines): self + { + $this->trendLines = $trendLines; + + return $this; + } + + /** @return TrendLine[] */ + public function getTrendLines(): array + { + return $this->trendLines; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + parent::__clone(); + $this->markerFillColor = clone $this->markerFillColor; + $this->markerBorderColor = clone $this->markerBorderColor; + if (is_array($this->fillColor)) { + $fillColor = $this->fillColor; + $this->fillColor = []; + foreach ($fillColor as $color) { + $this->fillColor[] = clone $color; + } + } elseif ($this->fillColor instanceof ChartColor) { + $this->fillColor = clone $this->fillColor; + } + $this->labelLayout = ($this->labelLayout === null) ? null : clone $this->labelLayout; + $trendLines = $this->trendLines; + $this->trendLines = []; + foreach ($trendLines as $trendLine) { + $this->trendLines[] = clone $trendLine; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Exception.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Exception.php new file mode 100644 index 0000000..3f95b59 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Exception.php @@ -0,0 +1,9 @@ + $layout + */ + public function __construct(array $layout = []) + { + /** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */ + if (isset($layout['layoutTarget'])) { + $this->layoutTarget = $layout['layoutTarget']; + } + if (isset($layout['xMode'])) { + $this->xMode = $layout['xMode']; + } + if (isset($layout['yMode'])) { + $this->yMode = $layout['yMode']; + } + if (isset($layout['x'])) { + $this->xPos = (float) $layout['x']; + } + if (isset($layout['y'])) { + $this->yPos = (float) $layout['y']; + } + if (isset($layout['w'])) { + $this->width = (float) $layout['w']; + } + if (isset($layout['h'])) { + $this->height = (float) $layout['h']; + } + if (isset($layout['dLblPos'])) { + $this->dLblPos = (string) $layout['dLblPos']; + } + if (isset($layout['numFmtCode'])) { + $this->numFmtCode = (string) $layout['numFmtCode']; + } + $this->initBoolean($layout, 'showLegendKey'); + $this->initBoolean($layout, 'showVal'); + $this->initBoolean($layout, 'showCatName'); + $this->initBoolean($layout, 'showSerName'); + $this->initBoolean($layout, 'showPercent'); + $this->initBoolean($layout, 'showBubbleSize'); + $this->initBoolean($layout, 'showLeaderLines'); + $this->initBoolean($layout, 'numFmtLinked'); + $this->initColor($layout, 'labelFillColor'); + $this->initColor($layout, 'labelBorderColor'); + $labelFont = $layout['labelFont'] ?? null; + if ($labelFont instanceof Font) { + $this->labelFont = $labelFont; + } + $labelFontColor = $layout['labelFontColor'] ?? null; + if ($labelFontColor instanceof ChartColor) { + $this->setLabelFontColor($labelFontColor); + } + $labelEffects = $layout['labelEffects'] ?? null; + if ($labelEffects instanceof Properties) { + $this->labelEffects = $labelEffects; + } + } + + /** @param mixed[] $layout */ + private function initBoolean(array $layout, string $name): void + { + if (isset($layout[$name])) { + $this->$name = (bool) $layout[$name]; + } + } + + /** @param mixed[] $layout */ + private function initColor(array $layout, string $name): void + { + if (isset($layout[$name]) && $layout[$name] instanceof ChartColor) { + $this->$name = $layout[$name]; + } + } + + /** + * Get Layout Target. + */ + public function getLayoutTarget(): ?string + { + return $this->layoutTarget; + } + + /** + * Set Layout Target. + * + * @return $this + */ + public function setLayoutTarget(?string $target): static + { + $this->layoutTarget = $target; + + return $this; + } + + /** + * Get X-Mode. + */ + public function getXMode(): ?string + { + return $this->xMode; + } + + /** + * Set X-Mode. + * + * @return $this + */ + public function setXMode(?string $mode): static + { + $this->xMode = (string) $mode; + + return $this; + } + + /** + * Get Y-Mode. + */ + public function getYMode(): ?string + { + return $this->yMode; + } + + /** + * Set Y-Mode. + * + * @return $this + */ + public function setYMode(?string $mode): static + { + $this->yMode = (string) $mode; + + return $this; + } + + /** + * Get X-Position. + */ + public function getXPosition(): null|float|int + { + return $this->xPos; + } + + /** + * Set X-Position. + * + * @return $this + */ + public function setXPosition(float $position): static + { + $this->xPos = $position; + + return $this; + } + + /** + * Get Y-Position. + */ + public function getYPosition(): ?float + { + return $this->yPos; + } + + /** + * Set Y-Position. + * + * @return $this + */ + public function setYPosition(float $position): static + { + $this->yPos = $position; + + return $this; + } + + /** + * Get Width. + */ + public function getWidth(): ?float + { + return $this->width; + } + + /** + * Set Width. + * + * @return $this + */ + public function setWidth(?float $width): static + { + $this->width = $width; + + return $this; + } + + /** + * Get Height. + */ + public function getHeight(): ?float + { + return $this->height; + } + + /** + * Set Height. + * + * @return $this + */ + public function setHeight(?float $height): static + { + $this->height = $height; + + return $this; + } + + public function getShowLegendKey(): ?bool + { + return $this->showLegendKey; + } + + /** + * Set show legend key + * Specifies that legend keys should be shown in data labels. + */ + public function setShowLegendKey(?bool $showLegendKey): self + { + $this->showLegendKey = $showLegendKey; + + return $this; + } + + public function getShowVal(): ?bool + { + return $this->showVal; + } + + /** + * Set show val + * Specifies that the value should be shown in data labels. + */ + public function setShowVal(?bool $showDataLabelValues): self + { + $this->showVal = $showDataLabelValues; + + return $this; + } + + public function getShowCatName(): ?bool + { + return $this->showCatName; + } + + /** + * Set show cat name + * Specifies that the category name should be shown in data labels. + */ + public function setShowCatName(?bool $showCategoryName): self + { + $this->showCatName = $showCategoryName; + + return $this; + } + + public function getShowSerName(): ?bool + { + return $this->showSerName; + } + + /** + * Set show data series name. + * Specifies that the series name should be shown in data labels. + */ + public function setShowSerName(?bool $showSeriesName): self + { + $this->showSerName = $showSeriesName; + + return $this; + } + + public function getShowPercent(): ?bool + { + return $this->showPercent; + } + + /** + * Set show percentage. + * Specifies that the percentage should be shown in data labels. + */ + public function setShowPercent(?bool $showPercentage): self + { + $this->showPercent = $showPercentage; + + return $this; + } + + public function getShowBubbleSize(): ?bool + { + return $this->showBubbleSize; + } + + /** + * Set show bubble size. + * Specifies that the bubble size should be shown in data labels. + */ + public function setShowBubbleSize(?bool $showBubbleSize): self + { + $this->showBubbleSize = $showBubbleSize; + + return $this; + } + + public function getShowLeaderLines(): ?bool + { + return $this->showLeaderLines; + } + + /** + * Set show leader lines. + * Specifies that leader lines should be shown in data labels. + */ + public function setShowLeaderLines(?bool $showLeaderLines): self + { + $this->showLeaderLines = $showLeaderLines; + + return $this; + } + + public function getLabelFillColor(): ?ChartColor + { + return $this->labelFillColor; + } + + public function setLabelFillColor(?ChartColor $chartColor): self + { + $this->labelFillColor = $chartColor; + + return $this; + } + + public function getLabelBorderColor(): ?ChartColor + { + return $this->labelBorderColor; + } + + public function setLabelBorderColor(?ChartColor $chartColor): self + { + $this->labelBorderColor = $chartColor; + + return $this; + } + + public function getLabelFont(): ?Font + { + return $this->labelFont; + } + + public function setLabelFont(?Font $labelFont): self + { + $this->labelFont = $labelFont; + + return $this; + } + + public function getLabelEffects(): ?Properties + { + return $this->labelEffects; + } + + public function getLabelFontColor(): ?ChartColor + { + if ($this->labelFont === null) { + return null; + } + + return $this->labelFont->getChartColor(); + } + + public function setLabelFontColor(?ChartColor $chartColor): self + { + if ($this->labelFont === null) { + $this->labelFont = new Font(); + $this->labelFont->setSize(null, true); + } + $this->labelFont->setChartColorFromObject($chartColor); + + return $this; + } + + public function getDLblPos(): string + { + return $this->dLblPos; + } + + public function setDLblPos(string $dLblPos): self + { + $this->dLblPos = $dLblPos; + + return $this; + } + + public function getNumFmtCode(): string + { + return $this->numFmtCode; + } + + public function setNumFmtCode(string $numFmtCode): self + { + $this->numFmtCode = $numFmtCode; + + return $this; + } + + public function getNumFmtLinked(): bool + { + return $this->numFmtLinked; + } + + public function setNumFmtLinked(bool $numFmtLinked): self + { + $this->numFmtLinked = $numFmtLinked; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->labelFillColor = ($this->labelFillColor === null) ? null : clone $this->labelFillColor; + $this->labelBorderColor = ($this->labelBorderColor === null) ? null : clone $this->labelBorderColor; + $this->labelFont = ($this->labelFont === null) ? null : clone $this->labelFont; + $this->labelEffects = ($this->labelEffects === null) ? null : clone $this->labelEffects; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Legend.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Legend.php new file mode 100644 index 0000000..f10642d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Legend.php @@ -0,0 +1,174 @@ + self::POSITION_BOTTOM, + self::XL_LEGEND_POSITION_CORNER => self::POSITION_TOPRIGHT, + self::XL_LEGEND_POSITION_CUSTOM => '??', + self::XL_LEGEND_POSITION_LEFT => self::POSITION_LEFT, + self::XL_LEGEND_POSITION_RIGHT => self::POSITION_RIGHT, + self::XL_LEGEND_POSITION_TOP => self::POSITION_TOP, + ]; + + /** + * Legend position. + */ + private string $position = self::POSITION_RIGHT; + + /** + * Allow overlay of other elements? + */ + private bool $overlay = true; + + /** + * Legend Layout. + */ + private ?Layout $layout; + + private GridLines $borderLines; + + private ChartColor $fillColor; + + private ?AxisText $legendText = null; + + /** + * Create a new Legend. + */ + public function __construct(string $position = self::POSITION_RIGHT, ?Layout $layout = null, bool $overlay = false) + { + $this->setPosition($position); + $this->layout = $layout; + $this->setOverlay($overlay); + $this->borderLines = new GridLines(); + $this->fillColor = new ChartColor(); + } + + public function getFillColor(): ChartColor + { + return $this->fillColor; + } + + /** + * Get legend position as an Excel string value. + */ + public function getPosition(): string + { + return $this->position; + } + + /** + * Get legend position using an Excel string value. + * + * @param string $position see self::POSITION_* + */ + public function setPosition(string $position): bool + { + if (!in_array($position, self::POSITION_XLREF)) { + return false; + } + + $this->position = $position; + + return true; + } + + /** + * Get legend position as an Excel internal numeric value. + */ + public function getPositionXL(): false|int + { + return array_search($this->position, self::POSITION_XLREF); + } + + /** + * Set legend position using an Excel internal numeric value. + * + * @param int $positionXL see self::XL_LEGEND_POSITION_* + */ + public function setPositionXL(int $positionXL): bool + { + if (!isset(self::POSITION_XLREF[$positionXL])) { + return false; + } + + $this->position = self::POSITION_XLREF[$positionXL]; + + return true; + } + + /** + * Get allow overlay of other elements? + */ + public function getOverlay(): bool + { + return $this->overlay; + } + + /** + * Set allow overlay of other elements? + */ + public function setOverlay(bool $overlay): void + { + $this->overlay = $overlay; + } + + /** + * Get Layout. + */ + public function getLayout(): ?Layout + { + return $this->layout; + } + + public function getLegendText(): ?AxisText + { + return $this->legendText; + } + + public function setLegendText(?AxisText $legendText): self + { + $this->legendText = $legendText; + + return $this; + } + + public function getBorderLines(): GridLines + { + return $this->borderLines; + } + + public function setBorderLines(GridLines $borderLines): self + { + $this->borderLines = $borderLines; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->layout = ($this->layout === null) ? null : clone $this->layout; + $this->legendText = ($this->legendText === null) ? null : clone $this->legendText; + $this->borderLines = clone $this->borderLines; + $this->fillColor = clone $this->fillColor; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/PlotArea.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/PlotArea.php new file mode 100644 index 0000000..e5e3907 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/PlotArea.php @@ -0,0 +1,210 @@ + + */ + private array $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + */ + private ?float $gradientFillAngle = null; + + /** + * PlotArea Layout. + */ + private ?Layout $layout; + + /** + * Plot Series. + * + * @var DataSeries[] + */ + private array $plotSeries; + + /** + * Create a new PlotArea. + * + * @param DataSeries[] $plotSeries + */ + public function __construct(?Layout $layout = null, array $plotSeries = []) + { + $this->layout = $layout; + $this->plotSeries = $plotSeries; + } + + public function getLayout(): ?Layout + { + return $this->layout; + } + + /** + * Get Number of Plot Groups. + */ + public function getPlotGroupCount(): int + { + return count($this->plotSeries); + } + + /** + * Get Number of Plot Series. + */ + public function getPlotSeriesCount(): int|float + { + $seriesCount = 0; + foreach ($this->plotSeries as $plot) { + $seriesCount += $plot->getPlotSeriesCount(); + } + + return $seriesCount; + } + + /** + * Get Plot Series. + * + * @return DataSeries[] + */ + public function getPlotGroup(): array + { + return $this->plotSeries; + } + + /** + * Get Plot Series by Index. + */ + public function getPlotGroupByIndex(int $index): DataSeries + { + return $this->plotSeries[$index]; + } + + /** + * Set Plot Series. + * + * @param DataSeries[] $plotSeries + * + * @return $this + */ + public function setPlotSeries(array $plotSeries): static + { + $this->plotSeries = $plotSeries; + + return $this; + } + + public function refresh(Worksheet $worksheet): void + { + foreach ($this->plotSeries as $plotSeries) { + $plotSeries->refresh($worksheet); + } + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + /** @param array $gradientFillStops */ + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops(): array + { + return $this->gradientFillStops; + } + + private ?int $gapWidth = null; + + private bool $useUpBars = false; + + private bool $useDownBars = false; + + public function getGapWidth(): ?int + { + return $this->gapWidth; + } + + public function setGapWidth(?int $gapWidth): self + { + $this->gapWidth = $gapWidth; + + return $this; + } + + public function getUseUpBars(): bool + { + return $this->useUpBars; + } + + public function setUseUpBars(bool $useUpBars): self + { + $this->useUpBars = $useUpBars; + + return $this; + } + + public function getUseDownBars(): bool + { + return $this->useDownBars; + } + + public function setUseDownBars(bool $useDownBars): self + { + $this->useDownBars = $useDownBars; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->layout = ($this->layout === null) ? null : clone $this->layout; + $plotSeries = $this->plotSeries; + $this->plotSeries = []; + foreach ($plotSeries as $series) { + $this->plotSeries[] = clone $series; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Properties.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Properties.php new file mode 100644 index 0000000..700ccd1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Properties.php @@ -0,0 +1,933 @@ + null, + ]; + + /** @var mixed[] */ + protected array $shadowProperties = self::PRESETS_OPTIONS[0]; + + protected ChartColor $shadowColor; + + public function __construct() + { + $this->lineColor = new ChartColor(); + $this->glowColor = new ChartColor(); + $this->shadowColor = new ChartColor(); + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + + /** + * Get Object State. + */ + public function getObjectState(): bool + { + return $this->objectState; + } + + /** + * Change Object State to True. + * + * @return $this + */ + public function activateObject() + { + $this->objectState = true; + + return $this; + } + + public static function pointsToXml(float $width): string + { + return (string) (int) ($width * self::POINTS_WIDTH_MULTIPLIER); + } + + public static function xmlToPoints(string $width): float + { + return ((float) $width) / self::POINTS_WIDTH_MULTIPLIER; + } + + public static function angleToXml(float $angle): string + { + return (string) (int) ($angle * self::ANGLE_MULTIPLIER); + } + + public static function xmlToAngle(string $angle): float + { + return ((float) $angle) / self::ANGLE_MULTIPLIER; + } + + public static function tenthOfPercentToXml(float $value): string + { + return (string) (int) ($value * self::PERCENTAGE_MULTIPLIER); + } + + public static function xmlToTenthOfPercent(string $value): float + { + return ((float) $value) / self::PERCENTAGE_MULTIPLIER; + } + + /** @return array{type: ?string, value: ?string, alpha: ?int} */ + protected function setColorProperties(?string $color, null|float|int|string $alpha, ?string $colorType): array + { + return [ + 'type' => $colorType, + 'value' => $color, + 'alpha' => ($alpha === null) ? null : (int) $alpha, + ]; + } + + protected const PRESETS_OPTIONS = [ + //NONE + 0 => [ + 'presets' => self::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + //'color' => [ + // 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, + // 'value' => 'black', + // 'alpha' => 40, + //], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ], + //OUTER + 1 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + 'algn' => 'tl', + 'rotWithShape' => '0', + ], + 2 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + 'algn' => 't', + 'rotWithShape' => '0', + ], + 3 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + 'algn' => 'tr', + 'rotWithShape' => '0', + ], + 4 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'algn' => 'l', + 'rotWithShape' => '0', + ], + 5 => [ + 'effect' => 'outerShdw', + 'size' => [ + 'sx' => 102000 / self::PERCENTAGE_MULTIPLIER, + 'sy' => 102000 / self::PERCENTAGE_MULTIPLIER, + ], + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'algn' => 'ctr', + 'rotWithShape' => '0', + ], + 6 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 10800000 / self::ANGLE_MULTIPLIER, + 'algn' => 'r', + 'rotWithShape' => '0', + ], + 7 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 8 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 16200000 / self::ANGLE_MULTIPLIER, + 'rotWithShape' => '0', + ], + 9 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + 'algn' => 'br', + 'rotWithShape' => '0', + ], + //INNER + 10 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + ], + 11 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + ], + 12 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + ], + 13 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + ], + 14 => [ + 'effect' => 'innerShdw', + 'blur' => 114300 / self::POINTS_WIDTH_MULTIPLIER, + ], + 15 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 10800000 / self::ANGLE_MULTIPLIER, + ], + 16 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + ], + 17 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 16200000 / self::ANGLE_MULTIPLIER, + ], + 18 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + ], + //perspective + 19 => [ + 'effect' => 'outerShdw', + 'blur' => 152400 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 317500 / self::POINTS_WIDTH_MULTIPLIER, + 'size' => [ + 'sx' => 90000 / self::PERCENTAGE_MULTIPLIER, + 'sy' => -19000 / self::PERCENTAGE_MULTIPLIER, + ], + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + 'rotWithShape' => '0', + ], + 20 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => 23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => -1200000 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 21 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => 23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => 1200000 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'br', + 'rotWithShape' => '0', + ], + 22 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => -23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => -800400 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 23 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => -23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => 800400 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'br', + 'rotWithShape' => '0', + ], + ]; + + /** @return mixed[] */ + protected function getShadowPresetsMap(int $presetsOption): array + { + return self::PRESETS_OPTIONS[$presetsOption] ?? self::PRESETS_OPTIONS[0]; + } + + /** + * Get value of array element. + * + * @param mixed[] $properties + * @param array|int|string $elements + */ + protected function getArrayElementsValue(array $properties, array|int|string $elements): mixed + { + $reference = &$properties; + if (!is_array($elements)) { + return $reference[$elements]; + } + + foreach ($elements as $keys) { + $reference = &$reference[$keys]; //* @phpstan-ignore-line + } + + return $reference; + } + + /** + * Set Glow Properties. + */ + public function setGlowProperties(float $size, ?string $colorValue = null, ?int $colorAlpha = null, ?string $colorType = null): void + { + $this + ->activateObject() + ->setGlowSize($size); + $this->glowColor->setColorPropertiesArray( + [ + 'value' => $colorValue, + 'type' => $colorType, + 'alpha' => $colorAlpha, + ] + ); + } + + /** + * Get Glow Property. + * + * @param mixed[]|string $property + * + * @return null|array|float|int|string + */ + public function getGlowProperty(array|string $property): null|array|float|int|string + { + $retVal = null; + if ($property === 'size') { + $retVal = $this->glowSize; + } elseif ($property === 'color') { + $retVal = [ + 'value' => $this->glowColor->getColorProperty('value'), + 'type' => $this->glowColor->getColorProperty('type'), + 'alpha' => $this->glowColor->getColorProperty('alpha'), + ]; + } elseif (is_array($property) && count($property) >= 2 && $property[0] === 'color') { + /** @var string */ + $temp = $property[1]; + $retVal = $this->glowColor->getColorProperty($temp); + } + + return $retVal; + } + + /** + * Get Glow Color Property. + */ + public function getGlowColor(string $propertyName): null|int|string + { + return $this->glowColor->getColorProperty($propertyName); + } + + public function getGlowColorObject(): ChartColor + { + return $this->glowColor; + } + + /** + * Get Glow Size. + */ + public function getGlowSize(): ?float + { + return $this->glowSize; + } + + /** + * Set Glow Size. + * + * @return $this + */ + protected function setGlowSize(?float $size) + { + $this->glowSize = $size; + + return $this; + } + + /** + * Set Soft Edges Size. + */ + public function setSoftEdges(?float $size): void + { + if ($size !== null) { + $this->activateObject(); + $this->softEdges['size'] = $size; + } + } + + /** + * Get Soft Edges Size. + */ + public function getSoftEdgesSize(): ?float + { + return $this->softEdges['size']; + } + + /** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string}|float|string $value */ + public function setShadowProperty(string $propertyName, mixed $value): self + { + $this->activateObject(); + if ($propertyName === 'color' && is_array($value)) { + /** @var array{value: ?string, alpha: null|int|string, brightness?: null|int|string, type: ?string} */ + $valuex = $value; + $this->shadowColor->setColorPropertiesArray($valuex); + } else { + $this->shadowProperties[$propertyName] = $value; + } + + return $this; + } + + /** + * Set Shadow Properties. + */ + public function setShadowProperties(int $presets, ?string $colorValue = null, ?string $colorType = null, null|float|int|string $colorAlpha = null, ?float $blur = null, ?int $angle = null, ?float $distance = null): void + { + $this->activateObject()->setShadowPresetsProperties((int) $presets); + if ($presets === 0) { + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + if ($colorValue !== null) { + $this->shadowColor->setValue($colorValue); + } + if ($colorType !== null) { + $this->shadowColor->setType($colorType); + } + if (is_numeric($colorAlpha)) { + $this->shadowColor->setAlpha((int) $colorAlpha); + } + $this + ->setShadowBlur($blur) + ->setShadowAngle($angle) + ->setShadowDistance($distance); + } + + /** + * Set Shadow Presets Properties. + * + * @return $this + */ + protected function setShadowPresetsProperties(int $presets) + { + $this->shadowProperties['presets'] = $presets; + $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + + return $this; + } + + protected const SHADOW_ARRAY_KEYS = ['size', 'color']; + + /** + * Set Shadow Properties Values. + * + * @param mixed[] $propertiesMap + * @param null|mixed[] $reference + * + * @return $this + */ + protected function setShadowPropertiesMapValues(array $propertiesMap, ?array &$reference = null) + { + $base_reference = $reference; + foreach ($propertiesMap as $property_key => $property_val) { + if (is_array($property_val)) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { + /** @var null|array */ + $temp = &$this->shadowProperties[$property_key]; + $reference = &$temp; + $this->setShadowPropertiesMapValues( + $property_val, + $reference + ); + } + } else { + if ($base_reference === null) { + $this->shadowProperties[$property_key] = $property_val; + } else { + $reference[$property_key] = $property_val; + } + } + } + + return $this; + } + + /** + * Set Shadow Blur. + * + * @return $this + */ + protected function setShadowBlur(?float $blur) + { + if ($blur !== null) { + $this->shadowProperties['blur'] = $blur; + } + + return $this; + } + + /** + * Set Shadow Angle. + * + * @return $this + */ + protected function setShadowAngle(null|float|int|string $angle) + { + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; + } + + return $this; + } + + /** + * Set Shadow Distance. + * + * @return $this + */ + protected function setShadowDistance(?float $distance) + { + if ($distance !== null) { + $this->shadowProperties['distance'] = $distance; + } + + return $this; + } + + public function getShadowColorObject(): ChartColor + { + return $this->shadowColor; + } + + /** + * Get Shadow Property. + * + * @param string|string[] $elements + * + * @return null|mixed[]|string + */ + public function getShadowProperty($elements): array|string|null + { + if ($elements === 'color') { + return [ + 'value' => $this->shadowColor->getValue(), + 'type' => $this->shadowColor->getType(), + 'alpha' => $this->shadowColor->getAlpha(), + ]; + } + $retVal = $this->getArrayElementsValue($this->shadowProperties, $elements); + if (is_scalar($retVal)) { + $retVal = (string) $retVal; + } elseif ($retVal !== null && !is_array($retVal)) { + // @codeCoverageIgnoreStart + throw new Exception('Unexpected value for shadowProperty'); + // @codeCoverageIgnoreEnd + } + + return $retVal; + } + + /** @return mixed[] */ + public function getShadowArray(): array + { + $array = $this->shadowProperties; + if ($this->getShadowColorObject()->isUsable()) { + $array['color'] = $this->getShadowProperty('color'); + } + + return $array; + } + + protected ChartColor $lineColor; + + /** @var array{width: null|float|int|string, compound: ?string, dash: ?string, cap: ?string, join: ?string, arrow: array{head: array{type: ?string, size: null|int|string, w: ?string, len: ?string}, end: array{type: ?string, size: null|int|string, w: ?string, len: ?string}}} */ + protected array $lineStyleProperties = [ + 'width' => null, //'9525', + 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => '', //self::LINE_STYLE_DASH_SOLID, + 'cap' => '', //self::LINE_STYLE_CAP_FLAT, + 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, + 'w' => '', + 'len' => '', + ], + 'end' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, + 'w' => '', + 'len' => '', + ], + ], + ]; + + public function copyLineStyles(self $otherProperties): void + { + $this->lineStyleProperties = $otherProperties->lineStyleProperties; + $this->lineColor = $otherProperties->lineColor; + $this->glowSize = $otherProperties->glowSize; + $this->glowColor = $otherProperties->glowColor; + $this->softEdges = $otherProperties->softEdges; + $this->shadowProperties = $otherProperties->shadowProperties; + } + + public function getLineColor(): ChartColor + { + return $this->lineColor; + } + + /** + * Set Line Color Properties. + */ + public function setLineColorProperties(?string $value, ?int $alpha = null, ?string $colorType = null): void + { + $this->activateObject(); + $this->lineColor->setColorPropertiesArray( + $this->setColorProperties( + $value, + $alpha, + $colorType + ) + ); + } + + /** + * Get Line Color Property. + */ + public function getLineColorProperty(string $propertyName): null|int|string + { + return $this->lineColor->getColorProperty($propertyName); + } + + /** + * Set Line Style Properties. + */ + public function setLineStyleProperties( + null|float|int|string $lineWidth = null, + ?string $compoundType = '', + ?string $dashType = '', + ?string $capType = '', + ?string $joinType = '', + ?string $headArrowType = '', + int $headArrowSize = 0, + ?string $endArrowType = '', + int $endArrowSize = 0, + ?string $headArrowWidth = '', + ?string $headArrowLength = '', + ?string $endArrowWidth = '', + ?string $endArrowLength = '' + ): void { + $this->activateObject(); + if (is_numeric($lineWidth)) { + $this->lineStyleProperties['width'] = $lineWidth; + } + if ($compoundType !== '') { + $this->lineStyleProperties['compound'] = $compoundType; + } + if ($dashType !== '') { + $this->lineStyleProperties['dash'] = $dashType; + } + if ($capType !== '') { + $this->lineStyleProperties['cap'] = $capType; + } + if ($joinType !== '') { + $this->lineStyleProperties['join'] = $joinType; + } + if ($headArrowType !== '') { + $this->lineStyleProperties['arrow']['head']['type'] = $headArrowType; + } + if (isset(self::ARROW_SIZES[$headArrowSize])) { + $this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize; + $this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; + $this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; + } + if ($endArrowType !== '') { + $this->lineStyleProperties['arrow']['end']['type'] = $endArrowType; + } + if (isset(self::ARROW_SIZES[$endArrowSize])) { + $this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize; + $this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; + $this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; + } + if ($headArrowWidth !== '') { + $this->lineStyleProperties['arrow']['head']['w'] = $headArrowWidth; + } + if ($headArrowLength !== '') { + $this->lineStyleProperties['arrow']['head']['len'] = $headArrowLength; + } + if ($endArrowWidth !== '') { + $this->lineStyleProperties['arrow']['end']['w'] = $endArrowWidth; + } + if ($endArrowLength !== '') { + $this->lineStyleProperties['arrow']['end']['len'] = $endArrowLength; + } + } + + /** @return mixed[] */ + public function getLineStyleArray(): array + { + return $this->lineStyleProperties; + } + + /** @param mixed[] $lineStyleProperties */ + public function setLineStyleArray(array $lineStyleProperties = []): self + { + /** @var array{width?: ?string, compound?: string, dash?: string, cap?: string, join?: string, arrow?: array{head?: array{type?: string, size?: int, w?: string, len?: string}, end?: array{type?: string, size?: int, w?: string, len?: string}}} $lineStyleProperties */ + $this->activateObject(); + $this->lineStyleProperties['width'] = $lineStyleProperties['width'] ?? null; + $this->lineStyleProperties['compound'] = $lineStyleProperties['compound'] ?? ''; + $this->lineStyleProperties['dash'] = $lineStyleProperties['dash'] ?? ''; + $this->lineStyleProperties['cap'] = $lineStyleProperties['cap'] ?? ''; + $this->lineStyleProperties['join'] = $lineStyleProperties['join'] ?? ''; + $this->lineStyleProperties['arrow']['head']['type'] = $lineStyleProperties['arrow']['head']['type'] ?? ''; + $this->lineStyleProperties['arrow']['head']['size'] = $lineStyleProperties['arrow']['head']['size'] ?? ''; + $this->lineStyleProperties['arrow']['head']['w'] = $lineStyleProperties['arrow']['head']['w'] ?? ''; + $this->lineStyleProperties['arrow']['head']['len'] = $lineStyleProperties['arrow']['head']['len'] ?? ''; + $this->lineStyleProperties['arrow']['end']['type'] = $lineStyleProperties['arrow']['end']['type'] ?? ''; + $this->lineStyleProperties['arrow']['end']['size'] = $lineStyleProperties['arrow']['end']['size'] ?? ''; + $this->lineStyleProperties['arrow']['end']['w'] = $lineStyleProperties['arrow']['end']['w'] ?? ''; + $this->lineStyleProperties['arrow']['end']['len'] = $lineStyleProperties['arrow']['end']['len'] ?? ''; + + return $this; + } + + public function setLineStyleProperty(string $propertyName, mixed $value): self + { + $this->activateObject(); + $this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore-line + + return $this; + } + + /** + * Get Line Style Property. + * + * @param array|string $elements + */ + public function getLineStyleProperty(array|string $elements): ?string + { + $retVal = $this->getArrayElementsValue($this->lineStyleProperties, $elements); + if (is_scalar($retVal)) { + $retVal = (string) $retVal; + } elseif ($retVal !== null) { + // @codeCoverageIgnoreStart + throw new Exception('Unexpected value for lineStyleProperty'); + // @codeCoverageIgnoreEnd + } + + return $retVal; + } + + protected const ARROW_SIZES = [ + 1 => ['w' => 'sm', 'len' => 'sm'], + 2 => ['w' => 'sm', 'len' => 'med'], + 3 => ['w' => 'sm', 'len' => 'lg'], + 4 => ['w' => 'med', 'len' => 'sm'], + 5 => ['w' => 'med', 'len' => 'med'], + 6 => ['w' => 'med', 'len' => 'lg'], + 7 => ['w' => 'lg', 'len' => 'sm'], + 8 => ['w' => 'lg', 'len' => 'med'], + 9 => ['w' => 'lg', 'len' => 'lg'], + ]; + + /** + * Get Line Style Arrow Size. + */ + protected function getLineStyleArrowSize(int $arraySelector, string $arrayKaySelector): string + { + return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? ''; + } + + /** + * Get Line Style Arrow Parameters. + */ + public function getLineStyleArrowParameters(string $arrowSelector, string $propertySelector): string + { + return $this->getLineStyleArrowSize((int) $this->lineStyleProperties['arrow'][$arrowSelector]['size'], $propertySelector); + } + + /** + * Get Line Style Arrow Width. + */ + public function getLineStyleArrowWidth(string $arrow): ?string + { + return $this->getLineStyleProperty(['arrow', $arrow, 'w']); + } + + /** + * Get Line Style Arrow Excel Length. + */ + public function getLineStyleArrowLength(string $arrow): ?string + { + return $this->getLineStyleProperty(['arrow', $arrow, 'len']); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->lineColor = clone $this->lineColor; + $this->glowColor = clone $this->glowColor; + $this->shadowColor = clone $this->shadowColor; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Renderer/IRenderer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Renderer/IRenderer.php new file mode 100644 index 0000000..ae28ff4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Renderer/IRenderer.php @@ -0,0 +1,22 @@ +graph = null; + $this->chart = $chart; + + self::$markSet = [ + 'diamond' => MARK_DIAMOND, + 'square' => MARK_SQUARE, + 'triangle' => MARK_UTRIANGLE, + 'x' => MARK_X, + 'star' => MARK_STAR, + 'dot' => MARK_FILLEDCIRCLE, + 'dash' => MARK_DTRIANGLE, + 'circle' => MARK_CIRCLE, + 'plus' => MARK_CROSS, + ]; + } + + private function getGraphWidth(): float + { + return $this->chart->getRenderedWidth() ?? self::DEFAULT_WIDTH; + } + + private function getGraphHeight(): float + { + return $this->chart->getRenderedHeight() ?? self::DEFAULT_HEIGHT; + } + + /** + * This method should be overridden in descendants to do real JpGraph library initialization. + */ + abstract protected static function init(): void; + + private function formatPointMarker($seriesPlot, $markerID) + { + $plotMarkKeys = array_keys(self::$markSet); + if ($markerID === null) { + // Use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } elseif ($markerID !== 'none') { + // Use specified plot marker (if it exists) + if (isset(self::$markSet[$markerID])) { + $seriesPlot->mark->SetType(self::$markSet[$markerID]); + } else { + // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } + } else { + // Hide plot marker + $seriesPlot->mark->Hide(); + } + $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); + $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + + return $seriesPlot; + } + + private function formatDataSetLabels(int $groupID, array $datasetLabels, $rotation = '') + { + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? ''; + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); + + $testCurrentIndex = 0; + foreach ($datasetLabels as $i => $datasetLabel) { + if (is_array($datasetLabel)) { + if ($rotation == 'bar') { + $datasetLabels[$i] = implode(' ', $datasetLabel); + } else { + $datasetLabel = array_reverse($datasetLabel); + $datasetLabels[$i] = implode("\n", $datasetLabel); + } + } else { + // Format labels according to any formatting code + if ($datasetLabelFormatCode !== null) { + $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); + } + } + ++$testCurrentIndex; + } + + return $datasetLabels; + } + + private function percentageSumCalculation(int $groupID, $seriesCount) + { + $sumValues = []; + // Adjust our values to a percentage value across all series in the group + for ($i = 0; $i < $seriesCount; ++$i) { + if ($i == 0) { + $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + } else { + $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + foreach ($nextValues as $k => $value) { + if (isset($sumValues[$k])) { + $sumValues[$k] += $value; + } else { + $sumValues[$k] = $value; + } + } + } + } + + return $sumValues; + } + + private function percentageAdjustValues(array $dataValues, array $sumValues) + { + foreach ($dataValues as $k => $dataValue) { + $dataValues[$k] = $dataValue / $sumValues[$k] * 100; + } + + return $dataValues; + } + + private function getCaption($captionElement) + { + // Read any caption + $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; + // Test if we have a title caption to display + if ($caption !== null) { + // If we do, it could be a plain string or an array + if (is_array($caption)) { + // Implode an array to a plain string + $caption = implode('', $caption); + } + } + + return $caption; + } + + private function renderTitle(): void + { + $title = $this->getCaption($this->chart->getTitle()); + if ($title !== null) { + $this->graph->title->Set($title); + } + } + + private function renderLegend(): void + { + $legend = $this->chart->getLegend(); + if ($legend !== null) { + $legendPosition = $legend->getPosition(); + switch ($legendPosition) { + case 'r': + $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right + $this->graph->legend->SetColumns(1); + + break; + case 'l': + $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left + $this->graph->legend->SetColumns(1); + + break; + case 't': + $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top + + break; + case 'b': + $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom + + break; + default: + $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right + $this->graph->legend->SetColumns(1); + + break; + } + } else { + $this->graph->legend->Hide(); + } + } + + private function renderCartesianPlotArea(string $type = 'textlin'): void + { + $this->graph = new Graph($this->getGraphWidth(), $this->getGraphHeight()); + $this->graph->SetScale($type); + + $this->renderTitle(); + + // Rotate for bar rather than column chart + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); + $reverse = $rotation == 'bar'; + + $xAxisLabel = $this->chart->getXAxisLabel(); + if ($xAxisLabel !== null) { + $title = $this->getCaption($xAxisLabel); + if ($title !== null) { + $this->graph->xaxis->SetTitle($title, 'center'); + $this->graph->xaxis->title->SetMargin(35); + if ($reverse) { + $this->graph->xaxis->title->SetAngle(90); + $this->graph->xaxis->title->SetMargin(90); + } + } + } + + $yAxisLabel = $this->chart->getYAxisLabel(); + if ($yAxisLabel !== null) { + $title = $this->getCaption($yAxisLabel); + if ($title !== null) { + $this->graph->yaxis->SetTitle($title, 'center'); + if ($reverse) { + $this->graph->yaxis->title->SetAngle(0); + $this->graph->yaxis->title->SetMargin(-55); + } + } + } + } + + private function renderPiePlotArea(): void + { + $this->graph = new PieGraph($this->getGraphWidth(), $this->getGraphHeight()); + + $this->renderTitle(); + } + + private function renderRadarPlotArea(): void + { + $this->graph = new RadarGraph($this->getGraphWidth(), $this->getGraphHeight()); + $this->graph->SetScale('lin'); + + $this->renderTitle(); + } + + private function getDataLabel(int $groupId, int $index): mixed + { + $plotLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupId)->getPlotLabelByIndex($index); + if (!$plotLabel) { + return ''; + } + + return $plotLabel->getDataValue(); + } + + private function renderPlotLine(int $groupID, bool $filled = false, bool $combination = false): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$i]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointMarker(); + + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + $seriesPlot = new LinePlot($dataValues); + if ($combination) { + $seriesPlot->SetBarCenter(); + } + + if ($filled) { + $seriesPlot->SetFilled(true); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + } else { + // Set the appropriate plot marker + $this->formatPointMarker($seriesPlot, $marker); + } + + $seriesPlot->SetLegend($this->getDataLabel($groupID, $index)); + + $seriesPlots[] = $seriesPlot; + } + + if ($grouping == 'standard') { + $groupPlot = $seriesPlots; + } else { + $groupPlot = new AccLinePlot($seriesPlots); + } + $this->graph->Add($groupPlot); + } + + private function renderPlotBar(int $groupID, ?string $dimensions = '2d'): void + { + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); + // Rotate for bar rather than column chart + if (($groupID == 0) && ($rotation == 'bar')) { + $this->graph->Set90AndMargin(); + } + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation); + // Rotate for bar rather than column chart + if ($rotation == 'bar') { + $datasetLabels = array_reverse($datasetLabels); + $this->graph->yaxis->SetPos('max'); + $this->graph->yaxis->SetLabelAlign('center', 'top'); + $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); + } + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($j = 0; $j < $seriesCount; ++$j) { + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$j]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + // Reverse the $dataValues order for bar rather than column chart + if ($rotation == 'bar') { + $dataValues = array_reverse($dataValues); + } + $seriesPlot = new BarPlot($dataValues); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + if ($dimensions == '3d') { + $seriesPlot->SetShadow(); + } + + $seriesPlot->SetLegend($this->getDataLabel($groupID, $j)); + + $seriesPlots[] = $seriesPlot; + } + // Reverse the plot order for bar rather than column chart + if (($rotation == 'bar') && ($grouping != 'percentStacked')) { + $seriesPlots = array_reverse($seriesPlots); + } + + if ($grouping == 'clustered') { + $groupPlot = new GroupBarPlot($seriesPlots); + } elseif ($grouping == 'standard') { + $groupPlot = new GroupBarPlot($seriesPlots); + } else { + $groupPlot = new AccBarPlot($seriesPlots); + if ($dimensions == '3d') { + $groupPlot->SetShadow(); + } + } + + $this->graph->Add($groupPlot); + } + + private function renderPlotScatter(int $groupID, bool $bubble): void + { + $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i); + if ($plotCategoryByIndex === false) { + $plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0); + } + $dataValuesY = $plotCategoryByIndex->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + $redoDataValuesY = true; + if ($bubble) { + if (!$bubbleSize) { + $bubbleSize = '10'; + } + $redoDataValuesY = false; + foreach ($dataValuesY as $dataValueY) { + if (!is_int($dataValueY) && !is_float($dataValueY)) { + $redoDataValuesY = true; + + break; + } + } + } + if ($redoDataValuesY) { + foreach ($dataValuesY as $k => $dataValueY) { + $dataValuesY[$k] = $k; + } + } + + $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); + if ($scatterStyle == 'lineMarker') { + $seriesPlot->SetLinkPoints(); + $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); + } elseif ($scatterStyle == 'smoothMarker') { + $spline = new Spline($dataValuesY, $dataValuesX); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * $this->getGraphWidth() / 20); + $lplot = new LinePlot($splineDataX, $splineDataY); + $lplot->SetColor(self::$colourSet[self::$plotColour]); + + $this->graph->Add($lplot); + } + + if ($bubble) { + $this->formatPointMarker($seriesPlot, 'dot'); + $seriesPlot->mark->SetColor('black'); + $seriesPlot->mark->SetSize($bubbleSize); + } else { + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + $this->formatPointMarker($seriesPlot, $marker); + } + $seriesPlot->SetLegend($this->getDataLabel($groupID, $i)); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotRadar(int $groupID): void + { + $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + $dataValues = []; + foreach ($dataValuesY as $k => $dataValueY) { + $dataValues[$k] = is_array($dataValueY) ? implode(' ', array_reverse($dataValueY)) : $dataValueY; + } + $tmp = array_shift($dataValues); + $dataValues[] = $tmp; + $tmp = array_shift($dataValuesX); + $dataValuesX[] = $tmp; + + $this->graph->SetTitles(array_reverse($dataValues)); + + $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); + + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if ($radarStyle == 'filled') { + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); + } + $this->formatPointMarker($seriesPlot, $marker); + $seriesPlot->SetLegend($this->getDataLabel($groupID, $i)); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotContour(int $groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + $dataValues = []; + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + $dataValues[$i] = $dataValuesX; + } + $seriesPlot = new ContourPlot($dataValues); + + $this->graph->Add($seriesPlot); + } + + private function renderPlotStock(int $groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); + + $dataValues = []; + // Loop through each data series in turn and build the plot arrays + foreach ($plotOrder as $i => $v) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v); + if ($dataValuesX === false) { + continue; + } + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); + foreach ($dataValuesX as $j => $dataValueX) { + $dataValues[$plotOrder[$i]][$j] = $dataValueX; + } + } + if (empty($dataValues)) { + return; + } + + $dataValuesPlot = []; + // Flatten the plot arrays to a single dimensional array to work with jpgraph + $jMax = count($dataValues[0]); + for ($j = 0; $j < $jMax; ++$j) { + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesPlot[] = $dataValues[$i][$j] ?? null; + } + } + + // Set the x-axis labels + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesPlot = new StockPlot($dataValuesPlot); + $seriesPlot->SetWidth(20); + + $this->graph->Add($seriesPlot); + } + + private function renderAreaChart($groupCount): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, true, false); + } + } + + private function renderLineChart($groupCount): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, false, false); + } + } + + private function renderBarChart($groupCount, ?string $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotBar($i, $dimensions); + } + } + + private function renderScatterChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, false); + } + } + + private function renderBubbleChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, true); + } + } + + private function renderPieChart($groupCount, ?string $dimensions = '2d', bool $doughnut = false, bool $multiplePlots = false): void + { + $this->renderPiePlotArea(); + + $iLimit = ($multiplePlots) ? $groupCount : 1; + for ($groupID = 0; $groupID < $iLimit; ++$groupID) { + $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + $datasetLabels = []; + if ($groupID == 0) { + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + } + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + // For pie charts, we only display the first series: doughnut charts generally display all series + $jLimit = ($multiplePlots) ? $seriesCount : 1; + // Loop through each data series in turn + for ($j = 0; $j < $jLimit; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + if ($dimensions == '3d') { + $seriesPlot = new PiePlot3D($dataValues); + } else { + if ($doughnut) { + $seriesPlot = new PiePlotC($dataValues); + } else { + $seriesPlot = new PiePlot($dataValues); + } + } + + if ($multiplePlots) { + $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); + } + + if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) { + $seriesPlot->SetMidColor('white'); + } + + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if (count($datasetLabels) > 0) { + $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); + } + if ($dimensions != '3d') { + $seriesPlot->SetGuideLines(false); + } + if ($j == 0) { + if ($exploded) { + $seriesPlot->ExplodeAll(); + } + $seriesPlot->SetLegends($datasetLabels); + } + + $this->graph->Add($seriesPlot); + } + } + } + + private function renderRadarChart($groupCount): void + { + $this->renderRadarPlotArea(); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotRadar($groupID); + } + } + + private function renderStockChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotStock($groupID); + } + } + + private function renderContourChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotContour($i); + } + } + + private function renderCombinationChart($groupCount, $outputDestination): bool + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $dimensions = null; + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + switch ($chartType) { + case 'area3DChart': + case 'areaChart': + $this->renderPlotLine($i, true, true); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderPlotBar($i, $dimensions); + + break; + case 'line3DChart': + case 'lineChart': + $this->renderPlotLine($i, false, true); + + break; + case 'scatterChart': + $this->renderPlotScatter($i, false); + + break; + case 'bubbleChart': + $this->renderPlotScatter($i, true); + + break; + default: + $this->graph = null; + + return false; + } + } + + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } + + public function render(?string $outputDestination): bool + { + self::$plotColour = 0; + + $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); + + $dimensions = null; + if ($groupCount == 1) { + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = array_pop($chartTypes); + } elseif (count($chartTypes) == 0) { + echo 'Chart is not yet implemented
'; + + return false; + } else { + return $this->renderCombinationChart($groupCount, $outputDestination); + } + } + + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderAreaChart($groupCount); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderBarChart($groupCount, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderLineChart($groupCount); + + break; + case 'pie3DChart': + $dimensions = '3d'; + // no break + case 'pieChart': + $this->renderPieChart($groupCount, $dimensions, false, false); + + break; + case 'doughnut3DChart': + $dimensions = '3d'; + // no break + case 'doughnutChart': + $this->renderPieChart($groupCount, $dimensions, true, true); + + break; + case 'scatterChart': + $this->renderScatterChart($groupCount); + + break; + case 'bubbleChart': + $this->renderBubbleChart($groupCount); + + break; + case 'radarChart': + $this->renderRadarChart($groupCount); + + break; + case 'surface3DChart': + case 'surfaceChart': + $this->renderContourChart($groupCount); + + break; + case 'stockChart': + $this->renderStockChart($groupCount); + + break; + default: + echo $chartType . ' is not yet implemented
'; + + return false; + } + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php new file mode 100644 index 0000000..9697920 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php @@ -0,0 +1,38 @@ +|RichText|string + */ + private array|RichText|string $caption; + + /** + * Allow overlay of other elements? + */ + private bool $overlay = true; + + /** + * Title Layout. + */ + private ?Layout $layout; + + private string $cellReference = ''; + + private ?Font $font = null; + + /** + * Create a new Title. + * + * @param array|RichText|string $caption + */ + public function __construct(array|RichText|string $caption = '', ?Layout $layout = null, bool $overlay = false) + { + $this->caption = $caption; + $this->layout = $layout; + $this->setOverlay($overlay); + } + + /** + * Get caption. + * + * @return array|RichText|string + */ + public function getCaption(): array|RichText|string + { + return $this->caption; + } + + public function getCaptionText(?Spreadsheet $spreadsheet = null): string + { + if ($spreadsheet !== null) { + $caption = $this->getCalculatedTitle($spreadsheet); + if ($caption !== null) { + return $caption; + } + } + $caption = $this->caption; + if (is_string($caption)) { + return $caption; + } + if ($caption instanceof RichText) { + return $caption->getPlainText(); + } + $retVal = ''; + foreach ($caption as $textx) { + /** @var RichText|string $text */ + $text = $textx; + if ($text instanceof RichText) { + $retVal .= $text->getPlainText(); + } else { + $retVal .= $text; + } + } + + return $retVal; + } + + /** + * Set caption. + * + * @param array|RichText|string $caption + * + * @return $this + */ + public function setCaption(array|RichText|string $caption): static + { + $this->caption = $caption; + + return $this; + } + + /** + * Get allow overlay of other elements? + */ + public function getOverlay(): bool + { + return $this->overlay; + } + + /** + * Set allow overlay of other elements? + */ + public function setOverlay(bool $overlay): self + { + $this->overlay = $overlay; + + return $this; + } + + public function getLayout(): ?Layout + { + return $this->layout; + } + + public function setCellReference(string $cellReference): self + { + $this->cellReference = $cellReference; + + return $this; + } + + public function getCellReference(): string + { + return $this->cellReference; + } + + public function getCalculatedTitle(?Spreadsheet $spreadsheet): ?string + { + preg_match(self::TITLE_CELL_REFERENCE, $this->cellReference, $matches); + if (count($matches) === 0 || $spreadsheet === null) { + return null; + } + $sheetName = preg_replace("/^'(.*)'$/", '$1', $matches[1]) ?? ''; + + return $spreadsheet->getSheetByName($sheetName)?->getCell($matches[2] . $matches[3])?->getFormattedValue(); + } + + public function getFont(): ?Font + { + return $this->font; + } + + public function setFont(?Font $font): self + { + $this->font = $font; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->layout = ($this->layout === null) ? null : clone $this->layout; + $this->font = ($this->font === null) ? null : clone $this->font; + if (is_array($this->caption)) { + $captions = []; + foreach ($this->caption as $caption) { + $captions[] = is_object($caption) ? (clone $caption) : $caption; + } + $this->caption = $captions; + } else { + $this->caption = is_object($this->caption) ? (clone $this->caption) : $this->caption; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/TrendLine.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/TrendLine.php new file mode 100644 index 0000000..5814fea --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Chart/TrendLine.php @@ -0,0 +1,217 @@ +setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); + } + + public function getTrendLineType(): string + { + return $this->trendLineType; + } + + public function setTrendLineType(string $trendLineType): self + { + $this->trendLineType = $trendLineType; + + return $this; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setOrder(int $order): self + { + $this->order = $order; + + return $this; + } + + public function getPeriod(): int + { + return $this->period; + } + + public function setPeriod(int $period): self + { + $this->period = $period; + + return $this; + } + + public function getDispRSqr(): bool + { + return $this->dispRSqr; + } + + public function setDispRSqr(bool $dispRSqr): self + { + $this->dispRSqr = $dispRSqr; + + return $this; + } + + public function getDispEq(): bool + { + return $this->dispEq; + } + + public function setDispEq(bool $dispEq): self + { + $this->dispEq = $dispEq; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getBackward(): float + { + return $this->backward; + } + + public function setBackward(float $backward): self + { + $this->backward = $backward; + + return $this; + } + + public function getForward(): float + { + return $this->forward; + } + + public function setForward(float $forward): self + { + $this->forward = $forward; + + return $this; + } + + public function getIntercept(): float + { + return $this->intercept; + } + + public function setIntercept(float $intercept): self + { + $this->intercept = $intercept; + + return $this; + } + + public function setTrendLineProperties( + ?string $trendLineType = null, + ?int $order = 0, + ?int $period = 0, + ?bool $dispRSqr = false, + ?bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ): self { + if (!empty($trendLineType)) { + $this->setTrendLineType($trendLineType); + } + if ($order !== null) { + $this->setOrder($order); + } + if ($period !== null) { + $this->setPeriod($period); + } + if ($dispRSqr !== null) { + $this->setDispRSqr($dispRSqr); + } + if ($dispEq !== null) { + $this->setDispEq($dispEq); + } + if ($backward !== null) { + $this->setBackward($backward); + } + if ($forward !== null) { + $this->setForward($forward); + } + if ($intercept !== null) { + $this->setIntercept($intercept); + } + if ($name !== null) { + $this->setName($name); + } + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/Cells.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/Cells.php new file mode 100644 index 0000000..269c267 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/Cells.php @@ -0,0 +1,546 @@ +parent = $parent; + $this->cache = $cache; + $this->cachePrefix = $this->getUniqueID(); + } + + /** + * Return the parent worksheet for this cell collection. + */ + public function getParent(): ?Worksheet + { + return $this->parent; + } + + /** + * Whether the collection holds a cell for the given coordinate. + * + * @param string $cellCoordinate Coordinate of the cell to check + */ + public function has(string $cellCoordinate): bool + { + return ($cellCoordinate === $this->currentCoordinate) || isset($this->index[$cellCoordinate]); + } + + public function has2(string $cellCoordinate): bool + { + return isset($this->index[$cellCoordinate]); + } + + /** + * Add or update a cell in the collection. + * + * @param Cell $cell Cell to update + */ + public function update(Cell $cell): Cell + { + return $this->add($cell->getCoordinate(), $cell); + } + + /** + * Delete a cell in cache identified by coordinate. + * + * @param string $cellCoordinate Coordinate of the cell to delete + */ + public function delete(string $cellCoordinate): void + { + if ($cellCoordinate === $this->currentCoordinate && $this->currentCell !== null) { + $this->currentCell->detach(); + $this->currentCoordinate = null; + $this->currentCell = null; + $this->currentCellIsDirty = false; + } + + unset($this->index[$cellCoordinate]); + + // Clear index caches + $this->indexKeysCache = null; + $this->indexValuesCache = null; + + // Delete the entry from cache + $this->cache->delete($this->cachePrefix . $cellCoordinate); + } + + /** + * Get a list of all cell coordinates currently held in the collection. + * + * @return string[] + */ + public function getCoordinates(): array + { + // Build or rebuild index keys cache + if ($this->indexKeysCache === null) { + $this->indexKeysCache = array_keys($this->index); + } + + return $this->indexKeysCache; + } + + /** + * Get a sorted list of all cell coordinates currently held in the collection by row and column. + * + * @return string[] + */ + public function getSortedCoordinates(): array + { + // Sort only when required + if (!$this->indexSorted) { + asort($this->index); + $this->indexSorted = true; + // Clear unsorted cache + $this->indexKeysCache = null; + $this->indexValuesCache = null; + } + + // Build or rebuild index keys cache + if ($this->indexKeysCache === null) { + $this->indexKeysCache = array_keys($this->index); + } + + return $this->indexKeysCache; + } + + /** + * Get a sorted list of all cell coordinates currently held in the collection by index (16384*row+column). + * + * @return int[] + */ + public function getSortedCoordinatesInt(): array + { + if (!$this->indexSorted) { + asort($this->index); + $this->indexSorted = true; + // Clear unsorted cache + $this->indexKeysCache = null; + $this->indexValuesCache = null; + } + + if ($this->indexValuesCache === null) { + $this->indexValuesCache = array_values($this->index); + } + + return $this->indexValuesCache; + } + + /** + * Return the cell coordinate of the currently active cell object. + */ + public function getCurrentCoordinate(): ?string + { + return $this->currentCoordinate; + } + + /** + * Return the column coordinate of the currently active cell object. + */ + public function getCurrentColumn(): string + { + $column = 0; + $row = ''; + sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); + + return (string) $column; + } + + /** + * Return the row coordinate of the currently active cell object. + */ + public function getCurrentRow(): int + { + $column = 0; + $row = ''; + sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); + + return (int) $row; + } + + /** + * Get highest worksheet column and highest row that have cell records. + * + * @return array{row: int, column: string} Highest column name and highest row number + */ + public function getHighestRowAndColumn(): array + { + // Lookup highest column and highest row + $maxRow = $maxColumn = 1; + foreach ($this->index as $coordinate) { + $row = (int) floor(($coordinate - 1) / self::MAX_COLUMN_ID) + 1; + $maxRow = ($maxRow > $row) ? $maxRow : $row; + $column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID; + $maxColumn = ($maxColumn > $column) ? $maxColumn : $column; + } + + return [ + 'row' => $maxRow, + 'column' => Coordinate::stringFromColumnIndex($maxColumn), + ]; + } + + /** + * Get highest worksheet column. + * + * @param null|int|string $row Return the highest column for the specified row, + * or the highest column of any row if no row number is passed + * + * @return string Highest column name + */ + public function getHighestColumn($row = null): string + { + if ($row === null) { + return $this->getHighestRowAndColumn()['column']; + } + + $row = (int) $row; + if ($row <= 0) { + throw new PhpSpreadsheetException('Row number must be a positive integer'); + } + + $maxColumn = 1; + $toRow = $row * self::MAX_COLUMN_ID; + $fromRow = --$row * self::MAX_COLUMN_ID; + foreach ($this->index as $coordinate) { + if ($coordinate < $fromRow || $coordinate >= $toRow) { + continue; + } + $column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID; + $maxColumn = max($column, $maxColumn); + } + + return Coordinate::stringFromColumnIndex($maxColumn); + } + + /** + * Get highest worksheet row. + * + * @param null|string $column Return the highest row for the specified column, + * or the highest row of any column if no column letter is passed + * + * @return int Highest row number + */ + public function getHighestRow(?string $column = null): int + { + if ($column === null) { + return $this->getHighestRowAndColumn()['row']; + } + + $maxRow = 1; + $columnIndex = Coordinate::columnIndexFromString($column); + foreach ($this->index as $coordinate) { + if ($coordinate % self::MAX_COLUMN_ID !== $columnIndex) { + continue; + } + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $maxRow = ($maxRow > $row) ? $maxRow : $row; + } + + return $maxRow; + } + + /** + * Generate a unique ID for cache referencing. + * + * @return string Unique Reference + */ + private function getUniqueID(): string + { + $cacheType = Settings::getCache(); + + return ($cacheType instanceof Memory\SimpleCache1 || $cacheType instanceof Memory\SimpleCache3) + ? random_bytes(7) . ':' + : uniqid('phpspreadsheet.', true) . '.'; + } + + /** + * Clone the cell collection. + */ + public function cloneCellCollection(Worksheet $worksheet): static + { + $this->storeCurrentCell(); + $newCollection = clone $this; + + $newCollection->parent = $worksheet; + $newCollection->cachePrefix = $newCollection->getUniqueID(); + + foreach ($this->index as $key => $value) { + $newCollection->index[$key] = $value; + $stored = $newCollection->cache->set( + $newCollection->cachePrefix . $key, + clone $this->getCache($key) + ); + if ($stored === false) { + $this->destructIfNeeded($newCollection, 'Failed to copy cells in cache'); + } + } + + // Clear index sorted flag and index caches + $newCollection->indexSorted = false; + $newCollection->indexKeysCache = null; + $newCollection->indexValuesCache = null; + + return $newCollection; + } + + /** + * Remove a row, deleting all cells in that row. + * + * @param int|string $row Row number to remove + */ + public function removeRow($row): void + { + $this->storeCurrentCell(); + $row = (int) $row; + if ($row <= 0) { + throw new PhpSpreadsheetException('Row number must be a positive integer'); + } + + $toRow = $row * self::MAX_COLUMN_ID; + $fromRow = --$row * self::MAX_COLUMN_ID; + foreach ($this->index as $coordinate) { + if ($coordinate >= $fromRow && $coordinate < $toRow) { + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID); + $this->delete("{$column}{$row}"); + } + } + } + + /** + * Remove a column, deleting all cells in that column. + * + * @param string $column Column ID to remove + */ + public function removeColumn(string $column): void + { + $this->storeCurrentCell(); + + $columnIndex = Coordinate::columnIndexFromString($column); + foreach ($this->index as $coordinate) { + if ($coordinate % self::MAX_COLUMN_ID === $columnIndex) { + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID); + $this->delete("{$column}{$row}"); + } + } + } + + /** + * Store cell data in cache for the current cell object if it's "dirty", + * and the 'nullify' the current cell object. + */ + private function storeCurrentCell(): void + { + if ($this->currentCellIsDirty && isset($this->currentCoordinate, $this->currentCell)) { + $this->currentCell->detach(); + + $stored = $this->cache->set($this->cachePrefix . $this->currentCoordinate, $this->currentCell); + if ($stored === false) { + $this->destructIfNeeded($this, "Failed to store cell {$this->currentCoordinate} in cache"); + } + $this->currentCellIsDirty = false; + } + + $this->currentCoordinate = null; + $this->currentCell = null; + } + + private function destructIfNeeded(self $cells, string $message): void + { + $cells->__destruct(); + + throw new PhpSpreadsheetException($message); + } + + /** + * Add or update a cell identified by its coordinate into the collection. + * + * @param string $cellCoordinate Coordinate of the cell to update + * @param Cell $cell Cell to update + */ + public function add(string $cellCoordinate, Cell $cell): Cell + { + if ($cellCoordinate !== $this->currentCoordinate) { + $this->storeCurrentCell(); + } + $column = 0; + $row = ''; + sscanf($cellCoordinate, '%[A-Z]%d', $column, $row); + /** @var int $row */ + $this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString((string) $column); + + // Clear index sorted flag and index caches + $this->indexSorted = false; + $this->indexKeysCache = null; + $this->indexValuesCache = null; + + $this->currentCoordinate = $cellCoordinate; + $this->currentCell = $cell; + $this->currentCellIsDirty = true; + + return $cell; + } + + /** + * Get cell at a specific coordinate. + * + * @param string $cellCoordinate Coordinate of the cell + * + * @return null|Cell Cell that was found, or null if not found + */ + public function get(string $cellCoordinate): ?Cell + { + if ($cellCoordinate === $this->currentCoordinate) { + return $this->currentCell; + } + $this->storeCurrentCell(); + + // Return null if requested entry doesn't exist in collection + if ($this->has($cellCoordinate) === false) { + return null; + } + + $cell = $this->getcache($cellCoordinate); + + // Set current entry to the requested entry + $this->currentCoordinate = $cellCoordinate; + $this->currentCell = $cell; + // Re-attach this as the cell's parent + $this->currentCell->attach($this); + + // Return requested entry + return $this->currentCell; + } + + /** + * Clear the cell collection and disconnect from our parent. + */ + public function unsetWorksheetCells(): void + { + if ($this->currentCell !== null) { + $this->currentCell->detach(); + $this->currentCell = null; + $this->currentCoordinate = null; + } + + // Flush the cache + $this->__destruct(); + + $this->index = []; + + // Clear index sorted flag and index caches + $this->indexSorted = false; + $this->indexKeysCache = null; + $this->indexValuesCache = null; + + // detach ourself from the worksheet, so that it can then delete this object successfully + $this->parent = null; + } + + /** + * Destroy this cell collection. + */ + public function __destruct() + { + $this->cache->deleteMultiple($this->getAllCacheKeys()); + $this->parent = null; + } + + /** + * Returns all known cache keys. + * + * @return iterable + */ + private function getAllCacheKeys(): iterable + { + foreach ($this->index as $coordinate => $value) { + yield $this->cachePrefix . $coordinate; + } + } + + private function getCache(string $cellCoordinate): Cell + { + $cell = $this->cache->get($this->cachePrefix . $cellCoordinate); + if (!($cell instanceof Cell)) { + throw new PhpSpreadsheetException("Cell entry {$cellCoordinate} no longer exists in cache. This probably means that the cache was cleared by someone else."); + } + + return $cell; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/CellsFactory.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/CellsFactory.php new file mode 100644 index 0000000..b3833bd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/CellsFactory.php @@ -0,0 +1,20 @@ +cache = []; + + return true; + } + + public function delete($key): bool + { + unset($this->cache[$key]); + + return true; + } + + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + public function get($key, $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + public function has($key): bool + { + return array_key_exists($key, $this->cache); + } + + public function set($key, $value, $ttl = null): bool + { + $this->cache[$key] = $value; + + return true; + } + + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php new file mode 100644 index 0000000..7b6c460 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php @@ -0,0 +1,80 @@ +cache = []; + + return true; + } + + public function delete(string $key): bool + { + unset($this->cache[$key]); + + return true; + } + + public function deleteMultiple(iterable $keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + public function get(string $key, mixed $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + public function getMultiple(iterable $keys, mixed $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->cache); + } + + public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool + { + $this->cache[$key] = $value; + + return true; + } + + public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Comment.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Comment.php new file mode 100644 index 0000000..2e18270 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Comment.php @@ -0,0 +1,358 @@ +author = 'Author'; + $this->text = new RichText(); + $this->fillColor = new Color('FFFFFFE1'); + $this->alignment = Alignment::HORIZONTAL_GENERAL; + $this->backgroundImage = new Drawing(); + } + + /** + * Get Author. + */ + public function getAuthor(): string + { + return $this->author; + } + + /** + * Set Author. + */ + public function setAuthor(string $author): self + { + $this->author = $author; + + return $this; + } + + /** + * Get Rich text comment. + */ + public function getText(): RichText + { + return $this->text; + } + + /** + * Set Rich text comment. + */ + public function setText(RichText $text): self + { + $this->text = $text; + + return $this; + } + + /** + * Get comment width (CSS style, i.e. XXpx or YYpt). + */ + public function getWidth(): string + { + return $this->width; + } + + /** + * Set comment width (CSS style, i.e. XXpx or YYpt). Default unit is pt. + */ + public function setWidth(string $width): self + { + $width = new Size($width); + if ($width->valid()) { + $this->width = (string) $width; + } + + return $this; + } + + /** + * Get comment height (CSS style, i.e. XXpx or YYpt). + */ + public function getHeight(): string + { + return $this->height; + } + + /** + * Set comment height (CSS style, i.e. XXpx or YYpt). Default unit is pt. + */ + public function setHeight(string $height): self + { + $height = new Size($height); + if ($height->valid()) { + $this->height = (string) $height; + } + + return $this; + } + + /** + * Get left margin (CSS style, i.e. XXpx or YYpt). + */ + public function getMarginLeft(): string + { + return $this->marginLeft; + } + + /** + * Set left margin (CSS style, i.e. XXpx or YYpt). Default unit is pt. + */ + public function setMarginLeft(string $margin): self + { + $margin = new Size($margin); + if ($margin->valid()) { + $this->marginLeft = (string) $margin; + } + + return $this; + } + + /** + * Get top margin (CSS style, i.e. XXpx or YYpt). + */ + public function getMarginTop(): string + { + return $this->marginTop; + } + + /** + * Set top margin (CSS style, i.e. XXpx or YYpt). Default unit is pt. + */ + public function setMarginTop(string $margin): self + { + $margin = new Size($margin); + if ($margin->valid()) { + $this->marginTop = (string) $margin; + } + + return $this; + } + + /** + * Is the comment visible by default? + */ + public function getVisible(): bool + { + return $this->visible; + } + + /** + * Set comment default visibility. + */ + public function setVisible(bool $visibility): self + { + $this->visible = $visibility; + + return $this; + } + + /** + * Set fill color. + */ + public function setFillColor(Color $color): self + { + $this->fillColor = $color; + + return $this; + } + + /** + * Get fill color. + */ + public function getFillColor(): Color + { + return $this->fillColor; + } + + public function setAlignment(string $alignment): self + { + $this->alignment = $alignment; + + return $this; + } + + public function getAlignment(): string + { + return $this->alignment; + } + + public function setTextboxDirection(string $textboxDirection): self + { + $this->textboxDirection = $textboxDirection; + + return $this; + } + + public function getTextboxDirection(): string + { + return $this->textboxDirection; + } + + /** + * Get hash code. + */ + public function getHashCode(): string + { + return md5( + $this->author + . $this->text->getHashCode() + . $this->width + . $this->height + . $this->marginLeft + . $this->marginTop + . ($this->visible ? 1 : 0) + . $this->fillColor->getHashCode() + . $this->alignment + . $this->textboxDirection + . ($this->hasBackgroundImage() ? $this->backgroundImage->getHashCode() : '') + . __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } + + /** + * Convert to string. + */ + public function __toString(): string + { + return $this->text->getPlainText(); + } + + /** + * Check is background image exists. + */ + public function hasBackgroundImage(): bool + { + $path = $this->backgroundImage->getPath(); + + if (empty($path)) { + return false; + } + + return getimagesize($path) !== false; + } + + /** + * Returns background image. + */ + public function getBackgroundImage(): Drawing + { + return $this->backgroundImage; + } + + /** + * Sets background image. + */ + public function setBackgroundImage(Drawing $objDrawing): self + { + if (!array_key_exists($objDrawing->getType(), Drawing::IMAGE_TYPES_CONVERTION_MAP)) { + throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + $this->backgroundImage = $objDrawing; + + return $this; + } + + /** + * Sets size of comment as size of background image. + */ + public function setSizeAsBackgroundImage(): self + { + if ($this->hasBackgroundImage()) { + $this->setWidth(SharedDrawing::pixelsToPoints($this->backgroundImage->getWidth()) . 'pt'); + $this->setHeight(SharedDrawing::pixelsToPoints($this->backgroundImage->getHeight()) . 'pt'); + } + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/DefinedName.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/DefinedName.php new file mode 100644 index 0000000..686e56e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/DefinedName.php @@ -0,0 +1,269 @@ +worksheet). + */ + protected bool $localOnly; + + /** + * Scope. + */ + protected ?Worksheet $scope; + + /** + * Whether this is a named range or a named formula. + */ + protected bool $isFormula; + + /** + * Create a new Defined Name. + */ + public function __construct( + string $name, + ?Worksheet $worksheet = null, + ?string $value = null, + bool $localOnly = false, + ?Worksheet $scope = null + ) { + if ($worksheet === null) { + $worksheet = $scope; + } + + // Set local members + $this->name = $name; + $this->worksheet = $worksheet; + $this->value = (string) $value; + $this->localOnly = $localOnly; + // If local only, then the scope will be set to worksheet unless a scope is explicitly set + $this->scope = ($localOnly === true) ? (($scope === null) ? $worksheet : $scope) : null; + // If the range string contains characters that aren't associated with the range definition (A-Z,1-9 + // for cell references, and $, or the range operators (colon comma or space), quotes and ! for + // worksheet names + // then this is treated as a named formula, and not a named range + $this->isFormula = self::testIfFormula($this->value); + } + + public function __destruct() + { + $this->worksheet = null; + $this->scope = null; + } + + /** + * Create a new defined name, either a range or a formula. + */ + public static function createInstance( + string $name, + ?Worksheet $worksheet = null, + ?string $value = null, + bool $localOnly = false, + ?Worksheet $scope = null + ): self { + $value = (string) $value; + $isFormula = self::testIfFormula($value); + if ($isFormula) { + return new NamedFormula($name, $worksheet, $value, $localOnly, $scope); + } + + return new NamedRange($name, $worksheet, $value, $localOnly, $scope); + } + + public static function testIfFormula(string $value): bool + { + if (str_starts_with($value, '=')) { + $value = substr($value, 1); + } + + if (is_numeric($value)) { + return true; + } + + $segMatcher = false; + foreach (explode("'", $value) as $subVal) { + // Only test in alternate array entries (the non-quoted blocks) + $segMatcher = $segMatcher === false; + if ( + $segMatcher + && (preg_match('/' . self::REGEXP_IDENTIFY_FORMULA . '/miu', $subVal)) + ) { + return true; + } + } + + return false; + } + + /** + * Get name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Set name. + */ + public function setName(string $name): self + { + if (!empty($name)) { + // Old title + $oldTitle = $this->name; + + // Re-attach + if ($this->worksheet !== null) { + $this->worksheet->getParentOrThrow()->removeNamedRange($this->name, $this->worksheet); + } + $this->name = $name; + + if ($this->worksheet !== null) { + $this->worksheet->getParentOrThrow()->addDefinedName($this); + } + + if ($this->worksheet !== null) { + // New title + $newTitle = $this->name; + ReferenceHelper::getInstance()->updateNamedFormulae($this->worksheet->getParentOrThrow(), $oldTitle, $newTitle); + } + } + + return $this; + } + + /** + * Get worksheet. + */ + public function getWorksheet(): ?Worksheet + { + return $this->worksheet; + } + + /** + * Set worksheet. + */ + public function setWorksheet(?Worksheet $worksheet): self + { + $this->worksheet = $worksheet; + + return $this; + } + + /** + * Get range or formula value. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Set range or formula value. + */ + public function setValue(string $value): self + { + $this->value = $value; + + return $this; + } + + /** + * Get localOnly. + */ + public function getLocalOnly(): bool + { + return $this->localOnly; + } + + /** + * Set localOnly. + */ + public function setLocalOnly(bool $localScope): self + { + $this->localOnly = $localScope; + $this->scope = $localScope ? $this->worksheet : null; + + return $this; + } + + /** + * Get scope. + */ + public function getScope(): ?Worksheet + { + return $this->scope; + } + + /** + * Set scope. + */ + public function setScope(?Worksheet $worksheet): self + { + $this->scope = $worksheet; + $this->localOnly = $worksheet !== null; + + return $this; + } + + /** + * Identify whether this is a named range or a named formula. + */ + public function isFormula(): bool + { + return $this->isFormula; + } + + /** + * Resolve a named range to a regular cell range or formula. + */ + public static function resolveName(string $definedName, Worksheet $worksheet, string $sheetName = ''): ?self + { + if ($sheetName === '') { + $worksheet2 = $worksheet; + } else { + $worksheet2 = $worksheet->getParentOrThrow()->getSheetByName($sheetName); + if ($worksheet2 === null) { + return null; + } + } + + return $worksheet->getParentOrThrow()->getDefinedName($definedName, $worksheet2); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Document/Properties.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Document/Properties.php new file mode 100644 index 0000000..6cad8a9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Document/Properties.php @@ -0,0 +1,509 @@ +lastModifiedBy = $this->creator; + $this->created = self::intOrFloatTimestamp(null); + $this->modified = $this->created; + } + + /** + * Get Creator. + */ + public function getCreator(): string + { + return $this->creator; + } + + /** + * Set Creator. + * + * @return $this + */ + public function setCreator(string $creator): self + { + $this->creator = $creator; + + return $this; + } + + /** + * Get Last Modified By. + */ + public function getLastModifiedBy(): string + { + return $this->lastModifiedBy; + } + + /** + * Set Last Modified By. + * + * @return $this + */ + public function setLastModifiedBy(string $modifiedBy): self + { + $this->lastModifiedBy = $modifiedBy; + + return $this; + } + + private static function intOrFloatTimestamp(null|bool|float|int|string $timestamp): float|int + { + if ($timestamp === null || is_bool($timestamp)) { + $timestamp = (float) (new DateTime())->format('U'); + } elseif (is_string($timestamp)) { + if (is_numeric($timestamp)) { + $timestamp = (float) $timestamp; + } else { + $timestamp = (string) preg_replace('/[.][0-9]*$/', '', $timestamp); + $timestamp = (string) preg_replace('/^(\d{4})- (\d)/', '$1-0$2', $timestamp); + $timestamp = (string) preg_replace('/^(\d{4}-\d{2})- (\d)/', '$1-0$2', $timestamp); + $timestamp = (float) (new DateTime($timestamp))->format('U'); + } + } + + return IntOrFloat::evaluate($timestamp); + } + + /** + * Get Created. + */ + public function getCreated(): float|int + { + return $this->created; + } + + /** + * Set Created. + * + * @return $this + */ + public function setCreated(null|float|int|string $timestamp): self + { + $this->created = self::intOrFloatTimestamp($timestamp); + + return $this; + } + + /** + * Get Modified. + */ + public function getModified(): float|int + { + return $this->modified; + } + + /** + * Set Modified. + * + * @return $this + */ + public function setModified(null|float|int|string $timestamp): self + { + $this->modified = self::intOrFloatTimestamp($timestamp); + + return $this; + } + + /** + * Get Title. + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * Set Title. + * + * @return $this + */ + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + /** + * Get Description. + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * Set Description. + * + * @return $this + */ + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + /** + * Get Subject. + */ + public function getSubject(): string + { + return $this->subject; + } + + /** + * Set Subject. + * + * @return $this + */ + public function setSubject(string $subject): self + { + $this->subject = $subject; + + return $this; + } + + /** + * Get Keywords. + */ + public function getKeywords(): string + { + return $this->keywords; + } + + /** + * Set Keywords. + * + * @return $this + */ + public function setKeywords(string $keywords): self + { + $this->keywords = $keywords; + + return $this; + } + + /** + * Get Category. + */ + public function getCategory(): string + { + return $this->category; + } + + /** + * Set Category. + * + * @return $this + */ + public function setCategory(string $category): self + { + $this->category = $category; + + return $this; + } + + /** + * Get Company. + */ + public function getCompany(): string + { + return $this->company; + } + + /** + * Set Company. + * + * @return $this + */ + public function setCompany(string $company): self + { + $this->company = $company; + + return $this; + } + + /** + * Get Manager. + */ + public function getManager(): string + { + return $this->manager; + } + + /** + * Set Manager. + * + * @return $this + */ + public function setManager(string $manager): self + { + $this->manager = $manager; + + return $this; + } + + /** + * Get a List of Custom Property Names. + * + * @return string[] + */ + public function getCustomProperties(): array + { + return array_keys($this->customProperties); + } + + /** + * Check if a Custom Property is defined. + */ + public function isCustomPropertySet(string $propertyName): bool + { + return array_key_exists($propertyName, $this->customProperties); + } + + /** + * Get a Custom Property Value. + */ + public function getCustomPropertyValue(string $propertyName): bool|int|float|string|null + { + if (isset($this->customProperties[$propertyName])) { + return $this->customProperties[$propertyName]['value']; + } + + return null; + } + + /** + * Get a Custom Property Type. + */ + public function getCustomPropertyType(string $propertyName): ?string + { + return $this->customProperties[$propertyName]['type'] ?? null; + } + + private function identifyPropertyType(bool|int|float|string|null $propertyValue): string + { + if (is_float($propertyValue)) { + return self::PROPERTY_TYPE_FLOAT; + } + if (is_int($propertyValue)) { + return self::PROPERTY_TYPE_INTEGER; + } + if (is_bool($propertyValue)) { + return self::PROPERTY_TYPE_BOOLEAN; + } + + return self::PROPERTY_TYPE_STRING; + } + + /** + * Set a Custom Property. + * + * @param ?string $propertyType see `self::VALID_PROPERTY_TYPE_LIST` + * + * @return $this + */ + public function setCustomProperty(string $propertyName, bool|int|float|string|null $propertyValue = '', ?string $propertyType = null): self + { + if (($propertyType === null) || (!in_array($propertyType, self::VALID_PROPERTY_TYPE_LIST))) { + $propertyType = $this->identifyPropertyType($propertyValue); + } + + $this->customProperties[$propertyName] = [ + 'value' => self::convertProperty($propertyValue, $propertyType), + 'type' => $propertyType, + ]; + + return $this; + } + + private const PROPERTY_TYPE_ARRAY = [ + 'i' => self::PROPERTY_TYPE_INTEGER, // Integer + 'i1' => self::PROPERTY_TYPE_INTEGER, // 1-Byte Signed Integer + 'i2' => self::PROPERTY_TYPE_INTEGER, // 2-Byte Signed Integer + 'i4' => self::PROPERTY_TYPE_INTEGER, // 4-Byte Signed Integer + 'i8' => self::PROPERTY_TYPE_INTEGER, // 8-Byte Signed Integer + 'int' => self::PROPERTY_TYPE_INTEGER, // Integer + 'ui1' => self::PROPERTY_TYPE_INTEGER, // 1-Byte Unsigned Integer + 'ui2' => self::PROPERTY_TYPE_INTEGER, // 2-Byte Unsigned Integer + 'ui4' => self::PROPERTY_TYPE_INTEGER, // 4-Byte Unsigned Integer + 'ui8' => self::PROPERTY_TYPE_INTEGER, // 8-Byte Unsigned Integer + 'uint' => self::PROPERTY_TYPE_INTEGER, // Unsigned Integer + 'f' => self::PROPERTY_TYPE_FLOAT, // Real Number + 'r4' => self::PROPERTY_TYPE_FLOAT, // 4-Byte Real Number + 'r8' => self::PROPERTY_TYPE_FLOAT, // 8-Byte Real Number + 'decimal' => self::PROPERTY_TYPE_FLOAT, // Decimal + 's' => self::PROPERTY_TYPE_STRING, // String + 'empty' => self::PROPERTY_TYPE_STRING, // Empty + 'null' => self::PROPERTY_TYPE_STRING, // Null + 'lpstr' => self::PROPERTY_TYPE_STRING, // LPSTR + 'lpwstr' => self::PROPERTY_TYPE_STRING, // LPWSTR + 'bstr' => self::PROPERTY_TYPE_STRING, // Basic String + 'd' => self::PROPERTY_TYPE_DATE, // Date and Time + 'date' => self::PROPERTY_TYPE_DATE, // Date and Time + 'filetime' => self::PROPERTY_TYPE_DATE, // File Time + 'b' => self::PROPERTY_TYPE_BOOLEAN, // Boolean + 'bool' => self::PROPERTY_TYPE_BOOLEAN, // Boolean + ]; + + private const SPECIAL_TYPES = [ + 'empty' => '', + 'null' => null, + ]; + + /** + * Convert property to form desired by Excel. + */ + public static function convertProperty(bool|int|float|string|null $propertyValue, string $propertyType): bool|int|float|string|null + { + return self::SPECIAL_TYPES[$propertyType] ?? self::convertProperty2($propertyValue, $propertyType); + } + + /** + * Convert property to form desired by Excel. + */ + private static function convertProperty2(bool|int|float|string|null $propertyValue, string $type): bool|int|float|string|null + { + $propertyType = self::convertPropertyType($type); + switch ($propertyType) { + case self::PROPERTY_TYPE_INTEGER: + $intValue = (int) $propertyValue; + + return ($type[0] === 'u') ? abs($intValue) : $intValue; + case self::PROPERTY_TYPE_FLOAT: + return (float) $propertyValue; + case self::PROPERTY_TYPE_DATE: + return self::intOrFloatTimestamp($propertyValue); + case self::PROPERTY_TYPE_BOOLEAN: + return is_bool($propertyValue) ? $propertyValue : ($propertyValue === 'true'); + default: // includes string + return $propertyValue; + } + } + + public static function convertPropertyType(string $propertyType): string + { + return self::PROPERTY_TYPE_ARRAY[$propertyType] ?? self::PROPERTY_TYPE_UNKNOWN; + } + + public function getHyperlinkBase(): string + { + return $this->hyperlinkBase; + } + + public function setHyperlinkBase(string $hyperlinkBase): self + { + $this->hyperlinkBase = $hyperlinkBase; + + return $this; + } + + public function getViewport(): string + { + return $this->viewport; + } + + public const SUGGESTED_VIEWPORT = 'width=device-width, initial-scale=1'; + + public function setViewport(string $viewport): self + { + $this->viewport = $viewport; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Document/Security.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Document/Security.php new file mode 100644 index 0000000..c65072a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Document/Security.php @@ -0,0 +1,257 @@ +lockRevision + || $this->lockStructure + || $this->lockWindows; + } + + public function getLockRevision(): bool + { + return $this->lockRevision; + } + + public function setLockRevision(?bool $locked): self + { + if ($locked !== null) { + $this->lockRevision = $locked; + } + + return $this; + } + + public function getLockStructure(): bool + { + return $this->lockStructure; + } + + public function setLockStructure(?bool $locked): self + { + if ($locked !== null) { + $this->lockStructure = $locked; + } + + return $this; + } + + public function getLockWindows(): bool + { + return $this->lockWindows; + } + + public function setLockWindows(?bool $locked): self + { + if ($locked !== null) { + $this->lockWindows = $locked; + } + + return $this; + } + + public function getRevisionsPassword(): string + { + return $this->revisionsPassword; + } + + /** + * Set RevisionsPassword. + * + * @param bool $alreadyHashed If the password has already been hashed, set this to true + * + * @return $this + */ + public function setRevisionsPassword(?string $password, bool $alreadyHashed = false): static + { + if ($password !== null) { + if ($this->advancedRevisionsPassword()) { + if (!$alreadyHashed) { + $password = PasswordHasher::hashPassword($password, $this->revisionsAlgorithmName, $this->revisionsSaltValue, $this->revisionsSpinCount); + } + $this->revisionsHashValue = $password; + $this->revisionsPassword = ''; + } else { + if (!$alreadyHashed) { + $password = PasswordHasher::hashPassword($password); + } + $this->revisionsPassword = $password; + } + } + + return $this; + } + + public function getWorkbookPassword(): string + { + return $this->workbookPassword; + } + + /** + * Set WorkbookPassword. + * + * @param bool $alreadyHashed If the password has already been hashed, set this to true + * + * @return $this + */ + public function setWorkbookPassword(?string $password, bool $alreadyHashed = false): static + { + if ($password !== null) { + if ($this->advancedPassword()) { + if (!$alreadyHashed) { + $password = PasswordHasher::hashPassword($password, $this->workbookAlgorithmName, $this->workbookSaltValue, $this->workbookSpinCount); + } + $this->workbookHashValue = $password; + $this->workbookPassword = ''; + } else { + if (!$alreadyHashed) { + $password = PasswordHasher::hashPassword($password); + } + $this->workbookPassword = $password; + } + } + + return $this; + } + + public function getWorkbookHashValue(): string + { + return $this->advancedPassword() ? $this->workbookHashValue : ''; + } + + public function advancedPassword(): bool + { + return $this->workbookAlgorithmName !== '' && $this->workbookSaltValue !== '' && $this->workbookSpinCount > 0; + } + + public function getWorkbookAlgorithmName(): string + { + return $this->workbookAlgorithmName; + } + + public function setWorkbookAlgorithmName(string $workbookAlgorithmName): static + { + $this->workbookAlgorithmName = $workbookAlgorithmName; + + return $this; + } + + public function getWorkbookSpinCount(): int + { + return $this->workbookSpinCount; + } + + public function setWorkbookSpinCount(int $workbookSpinCount): static + { + $this->workbookSpinCount = $workbookSpinCount; + + return $this; + } + + public function getWorkbookSaltValue(): string + { + return $this->workbookSaltValue; + } + + public function setWorkbookSaltValue(string $workbookSaltValue, bool $base64Required): static + { + $this->workbookSaltValue = $base64Required ? base64_encode($workbookSaltValue) : $workbookSaltValue; + + return $this; + } + + public function getRevisionsHashValue(): string + { + return $this->advancedRevisionsPassword() ? $this->revisionsHashValue : ''; + } + + public function advancedRevisionsPassword(): bool + { + return $this->revisionsAlgorithmName !== '' && $this->revisionsSaltValue !== '' && $this->revisionsSpinCount > 0; + } + + public function getRevisionsAlgorithmName(): string + { + return $this->revisionsAlgorithmName; + } + + public function setRevisionsAlgorithmName(string $revisionsAlgorithmName): static + { + $this->revisionsAlgorithmName = $revisionsAlgorithmName; + + return $this; + } + + public function getRevisionsSpinCount(): int + { + return $this->revisionsSpinCount; + } + + public function setRevisionsSpinCount(int $revisionsSpinCount): static + { + $this->revisionsSpinCount = $revisionsSpinCount; + + return $this; + } + + public function getRevisionsSaltValue(): string + { + return $this->revisionsSaltValue; + } + + public function setRevisionsSaltValue(string $revisionsSaltValue, bool $base64Required): static + { + $this->revisionsSaltValue = $base64Required ? base64_encode($revisionsSaltValue) : $revisionsSaltValue; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Exception.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Exception.php new file mode 100644 index 0000000..3491580 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Exception.php @@ -0,0 +1,9 @@ + + */ + protected array $items = []; + + /** + * HashTable key map. + * + * @var array + */ + protected array $keyMap = []; + + /** + * Create a new HashTable. + * + * @param T[] $source Optional source array to create HashTable from + */ + public function __construct(?array $source = []) + { + if ($source !== null) { + // Create HashTable + $this->addFromSource($source); + } + } + + /** + * Add HashTable items from source. + * + * @param T[] $source Source array to create HashTable from + */ + public function addFromSource(?array $source = null): void + { + // Check if an array was passed + if ($source === null) { + return; + } + + foreach ($source as $item) { + $this->add($item); + } + } + + /** + * Add HashTable item. + * + * @param T $source Item to add + */ + public function add(IComparable $source): void + { + $hash = $source->getHashCode(); + if (!isset($this->items[$hash])) { + $this->items[$hash] = $source; + $this->keyMap[count($this->items) - 1] = $hash; + } + } + + /** + * Remove HashTable item. + * + * @param T $source Item to remove + */ + public function remove(IComparable $source): void + { + $hash = $source->getHashCode(); + if (isset($this->items[$hash])) { + unset($this->items[$hash]); + + $deleteKey = -1; + foreach ($this->keyMap as $key => $value) { + if ($deleteKey >= 0) { + $this->keyMap[$key - 1] = $value; + } + + if ($value == $hash) { + $deleteKey = $key; + } + } + unset($this->keyMap[count($this->keyMap) - 1]); + } + } + + /** + * Clear HashTable. + */ + public function clear(): void + { + $this->items = []; + $this->keyMap = []; + } + + /** + * Count. + */ + public function count(): int + { + return count($this->items); + } + + /** + * Get index for hash code. + */ + public function getIndexForHashCode(string $hashCode): false|int + { + return array_search($hashCode, $this->keyMap, true); + } + + /** + * Get by index. + * + * @return null|T + */ + public function getByIndex(int $index): ?IComparable + { + if (isset($this->keyMap[$index])) { + return $this->getByHashCode($this->keyMap[$index]); + } + + return null; + } + + /** + * Get by hashcode. + * + * @return null|T + */ + public function getByHashCode(string $hashCode): ?IComparable + { + if (isset($this->items[$hashCode])) { + return $this->items[$hashCode]; + } + + return null; + } + + /** + * HashTable to array. + * + * @return T[] + */ + public function toArray(): array + { + return $this->items; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + // each member of this class is an array + if (is_array($value)) { + $array1 = $value; + foreach ($array1 as $key1 => $value1) { + if (is_object($value1)) { + $array1[$key1] = clone $value1; + } + } + $this->$key = $array1; + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Dimension.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Dimension.php new file mode 100644 index 0000000..e8c6c83 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Dimension.php @@ -0,0 +1,105 @@ + 96.0 / 2.54, + self::UOM_MILLIMETERS => 96.0 / 25.4, + self::UOM_INCHES => 96.0, + self::UOM_PIXELS => 1.0, + self::UOM_POINTS => 96.0 / 72, + self::UOM_PICA => 96.0 * 12 / 72, + ]; + + /** + * Based on a standard column width of 8.54 units in MS Excel. + */ + const RELATIVE_UNITS = [ + 'em' => 10.0 / 8.54, + 'ex' => 10.0 / 8.54, + 'ch' => 10.0 / 8.54, + 'rem' => 10.0 / 8.54, + 'vw' => 8.54, + 'vh' => 8.54, + 'vmin' => 8.54, + 'vmax' => 8.54, + '%' => 8.54 / 100, + ]; + + /** + * @var float|int If this is a width, then size is measured in pixels (if is set) + * or in Excel's default column width units if $unit is null. + * If this is a height, then size is measured in pixels () + * or in points () if $unit is null. + */ + protected float|int $size; + + protected ?string $unit = null; + + public function __construct(string $dimension) + { + $size = 0.0; + $unit = ''; + $sscanf = sscanf($dimension, '%[1234567890.]%s'); + if (is_array($sscanf)) { + $size = (float) ($sscanf[0] ?? 0.0); + $unit = strtolower($sscanf[1] ?? ''); + } + + // If a UoM is specified, then convert the size to pixels for internal storage + if (isset(self::ABSOLUTE_UNITS[$unit])) { + $size *= self::ABSOLUTE_UNITS[$unit]; + $this->unit = self::UOM_PIXELS; + } elseif (isset(self::RELATIVE_UNITS[$unit])) { + $size *= self::RELATIVE_UNITS[$unit]; + $size = round($size, 4); + } + + $this->size = $size; + } + + public function width(): float + { + return (float) ($this->unit === null) + ? $this->size + : round(Drawing::pixelsToCellDimension((int) $this->size, new Font(false)), 4); + } + + public function height(): float + { + return (float) ($this->unit === null) + ? $this->size + : $this->toUnit(self::UOM_POINTS); + } + + public function toUnit(string $unitOfMeasure): float + { + $unitOfMeasure = strtolower($unitOfMeasure); + if (!array_key_exists($unitOfMeasure, self::ABSOLUTE_UNITS)) { + throw new Exception("{$unitOfMeasure} is not a vaid unit of measure"); + } + + $size = $this->size; + if ($this->unit === null) { + $size = Drawing::cellDimensionToPixels($size, new Font(false)); + } + + return $size / self::ABSOLUTE_UNITS[$unitOfMeasure]; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Downloader.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Downloader.php new file mode 100644 index 0000000..0c39efe --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Downloader.php @@ -0,0 +1,102 @@ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xls' => 'application/vnd.ms-excel', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'csv' => 'text/csv', + 'html' => 'text/html', + 'pdf' => 'application/pdf', + ]; + + public function __construct(string $folder, string $filename, ?string $filetype = null) + { + if ((is_dir($folder) === false) || (is_readable($folder) === false)) { + throw new Exception('Folder is not accessible'); + } + $filepath = "{$folder}/{$filename}"; + $this->filepath = (string) realpath($filepath); + $this->filename = basename($filepath); + clearstatcache(); + if ((is_file($this->filepath) === false) || (is_readable($this->filepath) === false)) { + throw new Exception('File not found, or not a regular file, or cannot be read'); + } + + $filetype ??= pathinfo($filename, PATHINFO_EXTENSION); + if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) { + throw new Exception('Invalid filetype: file cannot be downloaded'); + } + $this->filetype = strtolower($filetype); + } + + public function download(): void + { + $this->headers(); + + readfile($this->filepath); + } + + public function headers(): void + { + // I cannot tell what this ob_clean is paired with. + // I have never seen a problem with it, but someone has - issue 3739. + // Perhaps it should be removed altogether, + // but making it conditional seems harmless, and safer. + if ((int) ob_get_length() > 0) { + ob_clean(); + } + + $this->contentType(); + $this->contentDisposition(); + $this->cacheHeaders(); + $this->fileSize(); + + flush(); + } + + protected function contentType(): void + { + header('Content-Type: ' . self::CONTENT_TYPES[$this->filetype]); + } + + protected function contentDisposition(): void + { + header('Content-Disposition: attachment;filename="' . $this->filename . '"'); + } + + protected function cacheHeaders(): void + { + header('Cache-Control: max-age=0'); + // If you're serving to IE 9, then the following may be needed + header('Cache-Control: max-age=1'); + + // If you're serving to IE over SSL, then the following may be needed + header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified + header('Cache-Control: cache, must-revalidate'); // HTTP/1.1 + header('Pragma: public'); // HTTP/1.0 + } + + protected function fileSize(): void + { + header('Content-Length: ' . filesize($this->filepath)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Handler.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Handler.php new file mode 100644 index 0000000..fb1b9a0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Handler.php @@ -0,0 +1,45 @@ + 'f0f8ff', + 'antiquewhite' => 'faebd7', + 'antiquewhite1' => 'ffefdb', + 'antiquewhite2' => 'eedfcc', + 'antiquewhite3' => 'cdc0b0', + 'antiquewhite4' => '8b8378', + 'aqua' => '00ffff', + 'aquamarine1' => '7fffd4', + 'aquamarine2' => '76eec6', + 'aquamarine4' => '458b74', + 'azure1' => 'f0ffff', + 'azure2' => 'e0eeee', + 'azure3' => 'c1cdcd', + 'azure4' => '838b8b', + 'beige' => 'f5f5dc', + 'bisque1' => 'ffe4c4', + 'bisque2' => 'eed5b7', + 'bisque3' => 'cdb79e', + 'bisque4' => '8b7d6b', + 'black' => '000000', + 'blanchedalmond' => 'ffebcd', + 'blue' => '0000ff', + 'blue1' => '0000ff', + 'blue2' => '0000ee', + 'blue4' => '00008b', + 'blueviolet' => '8a2be2', + 'brown' => 'a52a2a', + 'brown1' => 'ff4040', + 'brown2' => 'ee3b3b', + 'brown3' => 'cd3333', + 'brown4' => '8b2323', + 'burlywood' => 'deb887', + 'burlywood1' => 'ffd39b', + 'burlywood2' => 'eec591', + 'burlywood3' => 'cdaa7d', + 'burlywood4' => '8b7355', + 'cadetblue' => '5f9ea0', + 'cadetblue1' => '98f5ff', + 'cadetblue2' => '8ee5ee', + 'cadetblue3' => '7ac5cd', + 'cadetblue4' => '53868b', + 'chartreuse1' => '7fff00', + 'chartreuse2' => '76ee00', + 'chartreuse3' => '66cd00', + 'chartreuse4' => '458b00', + 'chocolate' => 'd2691e', + 'chocolate1' => 'ff7f24', + 'chocolate2' => 'ee7621', + 'chocolate3' => 'cd661d', + 'coral' => 'ff7f50', + 'coral1' => 'ff7256', + 'coral2' => 'ee6a50', + 'coral3' => 'cd5b45', + 'coral4' => '8b3e2f', + 'cornflowerblue' => '6495ed', + 'cornsilk1' => 'fff8dc', + 'cornsilk2' => 'eee8cd', + 'cornsilk3' => 'cdc8b1', + 'cornsilk4' => '8b8878', + 'cyan1' => '00ffff', + 'cyan2' => '00eeee', + 'cyan3' => '00cdcd', + 'cyan4' => '008b8b', + 'darkgoldenrod' => 'b8860b', + 'darkgoldenrod1' => 'ffb90f', + 'darkgoldenrod2' => 'eead0e', + 'darkgoldenrod3' => 'cd950c', + 'darkgoldenrod4' => '8b6508', + 'darkgreen' => '006400', + 'darkkhaki' => 'bdb76b', + 'darkolivegreen' => '556b2f', + 'darkolivegreen1' => 'caff70', + 'darkolivegreen2' => 'bcee68', + 'darkolivegreen3' => 'a2cd5a', + 'darkolivegreen4' => '6e8b3d', + 'darkorange' => 'ff8c00', + 'darkorange1' => 'ff7f00', + 'darkorange2' => 'ee7600', + 'darkorange3' => 'cd6600', + 'darkorange4' => '8b4500', + 'darkorchid' => '9932cc', + 'darkorchid1' => 'bf3eff', + 'darkorchid2' => 'b23aee', + 'darkorchid3' => '9a32cd', + 'darkorchid4' => '68228b', + 'darksalmon' => 'e9967a', + 'darkseagreen' => '8fbc8f', + 'darkseagreen1' => 'c1ffc1', + 'darkseagreen2' => 'b4eeb4', + 'darkseagreen3' => '9bcd9b', + 'darkseagreen4' => '698b69', + 'darkslateblue' => '483d8b', + 'darkslategray' => '2f4f4f', + 'darkslategray1' => '97ffff', + 'darkslategray2' => '8deeee', + 'darkslategray3' => '79cdcd', + 'darkslategray4' => '528b8b', + 'darkturquoise' => '00ced1', + 'darkviolet' => '9400d3', + 'deeppink1' => 'ff1493', + 'deeppink2' => 'ee1289', + 'deeppink3' => 'cd1076', + 'deeppink4' => '8b0a50', + 'deepskyblue1' => '00bfff', + 'deepskyblue2' => '00b2ee', + 'deepskyblue3' => '009acd', + 'deepskyblue4' => '00688b', + 'dimgray' => '696969', + 'dodgerblue1' => '1e90ff', + 'dodgerblue2' => '1c86ee', + 'dodgerblue3' => '1874cd', + 'dodgerblue4' => '104e8b', + 'firebrick' => 'b22222', + 'firebrick1' => 'ff3030', + 'firebrick2' => 'ee2c2c', + 'firebrick3' => 'cd2626', + 'firebrick4' => '8b1a1a', + 'floralwhite' => 'fffaf0', + 'forestgreen' => '228b22', + 'fuchsia' => 'ff00ff', + 'gainsboro' => 'dcdcdc', + 'ghostwhite' => 'f8f8ff', + 'gold1' => 'ffd700', + 'gold2' => 'eec900', + 'gold3' => 'cdad00', + 'gold4' => '8b7500', + 'goldenrod' => 'daa520', + 'goldenrod1' => 'ffc125', + 'goldenrod2' => 'eeb422', + 'goldenrod3' => 'cd9b1d', + 'goldenrod4' => '8b6914', + 'gray' => 'bebebe', + 'gray1' => '030303', + 'gray10' => '1a1a1a', + 'gray11' => '1c1c1c', + 'gray12' => '1f1f1f', + 'gray13' => '212121', + 'gray14' => '242424', + 'gray15' => '262626', + 'gray16' => '292929', + 'gray17' => '2b2b2b', + 'gray18' => '2e2e2e', + 'gray19' => '303030', + 'gray2' => '050505', + 'gray20' => '333333', + 'gray21' => '363636', + 'gray22' => '383838', + 'gray23' => '3b3b3b', + 'gray24' => '3d3d3d', + 'gray25' => '404040', + 'gray26' => '424242', + 'gray27' => '454545', + 'gray28' => '474747', + 'gray29' => '4a4a4a', + 'gray3' => '080808', + 'gray30' => '4d4d4d', + 'gray31' => '4f4f4f', + 'gray32' => '525252', + 'gray33' => '545454', + 'gray34' => '575757', + 'gray35' => '595959', + 'gray36' => '5c5c5c', + 'gray37' => '5e5e5e', + 'gray38' => '616161', + 'gray39' => '636363', + 'gray4' => '0a0a0a', + 'gray40' => '666666', + 'gray41' => '696969', + 'gray42' => '6b6b6b', + 'gray43' => '6e6e6e', + 'gray44' => '707070', + 'gray45' => '737373', + 'gray46' => '757575', + 'gray47' => '787878', + 'gray48' => '7a7a7a', + 'gray49' => '7d7d7d', + 'gray5' => '0d0d0d', + 'gray50' => '7f7f7f', + 'gray51' => '828282', + 'gray52' => '858585', + 'gray53' => '878787', + 'gray54' => '8a8a8a', + 'gray55' => '8c8c8c', + 'gray56' => '8f8f8f', + 'gray57' => '919191', + 'gray58' => '949494', + 'gray59' => '969696', + 'gray6' => '0f0f0f', + 'gray60' => '999999', + 'gray61' => '9c9c9c', + 'gray62' => '9e9e9e', + 'gray63' => 'a1a1a1', + 'gray64' => 'a3a3a3', + 'gray65' => 'a6a6a6', + 'gray66' => 'a8a8a8', + 'gray67' => 'ababab', + 'gray68' => 'adadad', + 'gray69' => 'b0b0b0', + 'gray7' => '121212', + 'gray70' => 'b3b3b3', + 'gray71' => 'b5b5b5', + 'gray72' => 'b8b8b8', + 'gray73' => 'bababa', + 'gray74' => 'bdbdbd', + 'gray75' => 'bfbfbf', + 'gray76' => 'c2c2c2', + 'gray77' => 'c4c4c4', + 'gray78' => 'c7c7c7', + 'gray79' => 'c9c9c9', + 'gray8' => '141414', + 'gray80' => 'cccccc', + 'gray81' => 'cfcfcf', + 'gray82' => 'd1d1d1', + 'gray83' => 'd4d4d4', + 'gray84' => 'd6d6d6', + 'gray85' => 'd9d9d9', + 'gray86' => 'dbdbdb', + 'gray87' => 'dedede', + 'gray88' => 'e0e0e0', + 'gray89' => 'e3e3e3', + 'gray9' => '171717', + 'gray90' => 'e5e5e5', + 'gray91' => 'e8e8e8', + 'gray92' => 'ebebeb', + 'gray93' => 'ededed', + 'gray94' => 'f0f0f0', + 'gray95' => 'f2f2f2', + 'gray97' => 'f7f7f7', + 'gray98' => 'fafafa', + 'gray99' => 'fcfcfc', + 'green' => '00ff00', + 'green1' => '00ff00', + 'green2' => '00ee00', + 'green3' => '00cd00', + 'green4' => '008b00', + 'greenyellow' => 'adff2f', + 'honeydew1' => 'f0fff0', + 'honeydew2' => 'e0eee0', + 'honeydew3' => 'c1cdc1', + 'honeydew4' => '838b83', + 'hotpink' => 'ff69b4', + 'hotpink1' => 'ff6eb4', + 'hotpink2' => 'ee6aa7', + 'hotpink3' => 'cd6090', + 'hotpink4' => '8b3a62', + 'indianred' => 'cd5c5c', + 'indianred1' => 'ff6a6a', + 'indianred2' => 'ee6363', + 'indianred3' => 'cd5555', + 'indianred4' => '8b3a3a', + 'ivory1' => 'fffff0', + 'ivory2' => 'eeeee0', + 'ivory3' => 'cdcdc1', + 'ivory4' => '8b8b83', + 'khaki' => 'f0e68c', + 'khaki1' => 'fff68f', + 'khaki2' => 'eee685', + 'khaki3' => 'cdc673', + 'khaki4' => '8b864e', + 'lavender' => 'e6e6fa', + 'lavenderblush1' => 'fff0f5', + 'lavenderblush2' => 'eee0e5', + 'lavenderblush3' => 'cdc1c5', + 'lavenderblush4' => '8b8386', + 'lawngreen' => '7cfc00', + 'lemonchiffon1' => 'fffacd', + 'lemonchiffon2' => 'eee9bf', + 'lemonchiffon3' => 'cdc9a5', + 'lemonchiffon4' => '8b8970', + 'light' => 'eedd82', + 'lightblue' => 'add8e6', + 'lightblue1' => 'bfefff', + 'lightblue2' => 'b2dfee', + 'lightblue3' => '9ac0cd', + 'lightblue4' => '68838b', + 'lightcoral' => 'f08080', + 'lightcyan1' => 'e0ffff', + 'lightcyan2' => 'd1eeee', + 'lightcyan3' => 'b4cdcd', + 'lightcyan4' => '7a8b8b', + 'lightgoldenrod1' => 'ffec8b', + 'lightgoldenrod2' => 'eedc82', + 'lightgoldenrod3' => 'cdbe70', + 'lightgoldenrod4' => '8b814c', + 'lightgoldenrodyellow' => 'fafad2', + 'lightgray' => 'd3d3d3', + 'lightpink' => 'ffb6c1', + 'lightpink1' => 'ffaeb9', + 'lightpink2' => 'eea2ad', + 'lightpink3' => 'cd8c95', + 'lightpink4' => '8b5f65', + 'lightsalmon1' => 'ffa07a', + 'lightsalmon2' => 'ee9572', + 'lightsalmon3' => 'cd8162', + 'lightsalmon4' => '8b5742', + 'lightseagreen' => '20b2aa', + 'lightskyblue' => '87cefa', + 'lightskyblue1' => 'b0e2ff', + 'lightskyblue2' => 'a4d3ee', + 'lightskyblue3' => '8db6cd', + 'lightskyblue4' => '607b8b', + 'lightslateblue' => '8470ff', + 'lightslategray' => '778899', + 'lightsteelblue' => 'b0c4de', + 'lightsteelblue1' => 'cae1ff', + 'lightsteelblue2' => 'bcd2ee', + 'lightsteelblue3' => 'a2b5cd', + 'lightsteelblue4' => '6e7b8b', + 'lightyellow1' => 'ffffe0', + 'lightyellow2' => 'eeeed1', + 'lightyellow3' => 'cdcdb4', + 'lightyellow4' => '8b8b7a', + 'lime' => '00ff00', + 'limegreen' => '32cd32', + 'linen' => 'faf0e6', + 'magenta' => 'ff00ff', + 'magenta2' => 'ee00ee', + 'magenta3' => 'cd00cd', + 'magenta4' => '8b008b', + 'maroon' => 'b03060', + 'maroon1' => 'ff34b3', + 'maroon2' => 'ee30a7', + 'maroon3' => 'cd2990', + 'maroon4' => '8b1c62', + 'medium' => '66cdaa', + 'mediumaquamarine' => '66cdaa', + 'mediumblue' => '0000cd', + 'mediumorchid' => 'ba55d3', + 'mediumorchid1' => 'e066ff', + 'mediumorchid2' => 'd15fee', + 'mediumorchid3' => 'b452cd', + 'mediumorchid4' => '7a378b', + 'mediumpurple' => '9370db', + 'mediumpurple1' => 'ab82ff', + 'mediumpurple2' => '9f79ee', + 'mediumpurple3' => '8968cd', + 'mediumpurple4' => '5d478b', + 'mediumseagreen' => '3cb371', + 'mediumslateblue' => '7b68ee', + 'mediumspringgreen' => '00fa9a', + 'mediumturquoise' => '48d1cc', + 'mediumvioletred' => 'c71585', + 'midnightblue' => '191970', + 'mintcream' => 'f5fffa', + 'mistyrose1' => 'ffe4e1', + 'mistyrose2' => 'eed5d2', + 'mistyrose3' => 'cdb7b5', + 'mistyrose4' => '8b7d7b', + 'moccasin' => 'ffe4b5', + 'navajowhite1' => 'ffdead', + 'navajowhite2' => 'eecfa1', + 'navajowhite3' => 'cdb38b', + 'navajowhite4' => '8b795e', + 'navy' => '000080', + 'navyblue' => '000080', + 'oldlace' => 'fdf5e6', + 'olive' => '808000', + 'olivedrab' => '6b8e23', + 'olivedrab1' => 'c0ff3e', + 'olivedrab2' => 'b3ee3a', + 'olivedrab4' => '698b22', + 'orange' => 'ffa500', + 'orange1' => 'ffa500', + 'orange2' => 'ee9a00', + 'orange3' => 'cd8500', + 'orange4' => '8b5a00', + 'orangered1' => 'ff4500', + 'orangered2' => 'ee4000', + 'orangered3' => 'cd3700', + 'orangered4' => '8b2500', + 'orchid' => 'da70d6', + 'orchid1' => 'ff83fa', + 'orchid2' => 'ee7ae9', + 'orchid3' => 'cd69c9', + 'orchid4' => '8b4789', + 'pale' => 'db7093', + 'palegoldenrod' => 'eee8aa', + 'palegreen' => '98fb98', + 'palegreen1' => '9aff9a', + 'palegreen2' => '90ee90', + 'palegreen3' => '7ccd7c', + 'palegreen4' => '548b54', + 'paleturquoise' => 'afeeee', + 'paleturquoise1' => 'bbffff', + 'paleturquoise2' => 'aeeeee', + 'paleturquoise3' => '96cdcd', + 'paleturquoise4' => '668b8b', + 'palevioletred' => 'db7093', + 'palevioletred1' => 'ff82ab', + 'palevioletred2' => 'ee799f', + 'palevioletred3' => 'cd6889', + 'palevioletred4' => '8b475d', + 'papayawhip' => 'ffefd5', + 'peachpuff1' => 'ffdab9', + 'peachpuff2' => 'eecbad', + 'peachpuff3' => 'cdaf95', + 'peachpuff4' => '8b7765', + 'pink' => 'ffc0cb', + 'pink1' => 'ffb5c5', + 'pink2' => 'eea9b8', + 'pink3' => 'cd919e', + 'pink4' => '8b636c', + 'plum' => 'dda0dd', + 'plum1' => 'ffbbff', + 'plum2' => 'eeaeee', + 'plum3' => 'cd96cd', + 'plum4' => '8b668b', + 'powderblue' => 'b0e0e6', + 'purple' => 'a020f0', + 'rebeccapurple' => '663399', + 'purple1' => '9b30ff', + 'purple2' => '912cee', + 'purple3' => '7d26cd', + 'purple4' => '551a8b', + 'red' => 'ff0000', + 'red1' => 'ff0000', + 'red2' => 'ee0000', + 'red3' => 'cd0000', + 'red4' => '8b0000', + 'rosybrown' => 'bc8f8f', + 'rosybrown1' => 'ffc1c1', + 'rosybrown2' => 'eeb4b4', + 'rosybrown3' => 'cd9b9b', + 'rosybrown4' => '8b6969', + 'royalblue' => '4169e1', + 'royalblue1' => '4876ff', + 'royalblue2' => '436eee', + 'royalblue3' => '3a5fcd', + 'royalblue4' => '27408b', + 'saddlebrown' => '8b4513', + 'salmon' => 'fa8072', + 'salmon1' => 'ff8c69', + 'salmon2' => 'ee8262', + 'salmon3' => 'cd7054', + 'salmon4' => '8b4c39', + 'sandybrown' => 'f4a460', + 'seagreen1' => '54ff9f', + 'seagreen2' => '4eee94', + 'seagreen3' => '43cd80', + 'seagreen4' => '2e8b57', + 'seashell1' => 'fff5ee', + 'seashell2' => 'eee5de', + 'seashell3' => 'cdc5bf', + 'seashell4' => '8b8682', + 'sienna' => 'a0522d', + 'sienna1' => 'ff8247', + 'sienna2' => 'ee7942', + 'sienna3' => 'cd6839', + 'sienna4' => '8b4726', + 'silver' => 'c0c0c0', + 'skyblue' => '87ceeb', + 'skyblue1' => '87ceff', + 'skyblue2' => '7ec0ee', + 'skyblue3' => '6ca6cd', + 'skyblue4' => '4a708b', + 'slateblue' => '6a5acd', + 'slateblue1' => '836fff', + 'slateblue2' => '7a67ee', + 'slateblue3' => '6959cd', + 'slateblue4' => '473c8b', + 'slategray' => '708090', + 'slategray1' => 'c6e2ff', + 'slategray2' => 'b9d3ee', + 'slategray3' => '9fb6cd', + 'slategray4' => '6c7b8b', + 'snow1' => 'fffafa', + 'snow2' => 'eee9e9', + 'snow3' => 'cdc9c9', + 'snow4' => '8b8989', + 'springgreen1' => '00ff7f', + 'springgreen2' => '00ee76', + 'springgreen3' => '00cd66', + 'springgreen4' => '008b45', + 'steelblue' => '4682b4', + 'steelblue1' => '63b8ff', + 'steelblue2' => '5cacee', + 'steelblue3' => '4f94cd', + 'steelblue4' => '36648b', + 'tan' => 'd2b48c', + 'tan1' => 'ffa54f', + 'tan2' => 'ee9a49', + 'tan3' => 'cd853f', + 'tan4' => '8b5a2b', + 'teal' => '008080', + 'thistle' => 'd8bfd8', + 'thistle1' => 'ffe1ff', + 'thistle2' => 'eed2ee', + 'thistle3' => 'cdb5cd', + 'thistle4' => '8b7b8b', + 'tomato1' => 'ff6347', + 'tomato2' => 'ee5c42', + 'tomato3' => 'cd4f39', + 'tomato4' => '8b3626', + 'turquoise' => '40e0d0', + 'turquoise1' => '00f5ff', + 'turquoise2' => '00e5ee', + 'turquoise3' => '00c5cd', + 'turquoise4' => '00868b', + 'violet' => 'ee82ee', + 'violetred' => 'd02090', + 'violetred1' => 'ff3e96', + 'violetred2' => 'ee3a8c', + 'violetred3' => 'cd3278', + 'violetred4' => '8b2252', + 'wheat' => 'f5deb3', + 'wheat1' => 'ffe7ba', + 'wheat2' => 'eed8ae', + 'wheat3' => 'cdba96', + 'wheat4' => '8b7e66', + 'white' => 'ffffff', + 'whitesmoke' => 'f5f5f5', + 'yellow' => 'ffff00', + 'yellow1' => 'ffff00', + 'yellow2' => 'eeee00', + 'yellow3' => 'cdcd00', + 'yellow4' => '8b8b00', + 'yellowgreen' => '9acd32', + ]; + + private ?string $face = null; + + private ?string $size = null; + + private ?string $color = null; + + private bool $bold = false; + + private bool $italic = false; + + private bool $underline = false; + + private bool $superscript = false; + + private bool $subscript = false; + + private bool $strikethrough = false; + + /** @var callable[] */ + protected array $startTagCallbacks; + + /** @var callable[] */ + protected array $endTagCallbacks; + + /** @var mixed[] */ + private array $stack = []; + + public string $stringData = ''; + + private RichText $richTextObject; + + private bool $preserveWhiteSpace = false; + + public function __construct() + { + if (!isset($this->startTagCallbacks)) { + $this->startTagCallbacks = [ + 'font' => $this->startFontTag(...), + 'b' => $this->startBoldTag(...), + 'strong' => $this->startBoldTag(...), + 'i' => $this->startItalicTag(...), + 'em' => $this->startItalicTag(...), + 'u' => $this->startUnderlineTag(...), + 'ins' => $this->startUnderlineTag(...), + 'del' => $this->startStrikethruTag(...), + 's' => $this->startStrikethruTag(...), + 'sup' => $this->startSuperscriptTag(...), + 'sub' => $this->startSubscriptTag(...), + ]; + } + if (!isset($this->endTagCallbacks)) { + $this->endTagCallbacks = [ + 'font' => $this->endFontTag(...), + 'b' => $this->endBoldTag(...), + 'strong' => $this->endBoldTag(...), + 'i' => $this->endItalicTag(...), + 'em' => $this->endItalicTag(...), + 'u' => $this->endUnderlineTag(...), + 'ins' => $this->endUnderlineTag(...), + 'del' => $this->endStrikethruTag(...), + 's' => $this->endStrikethruTag(...), + 'sup' => $this->endSuperscriptTag(...), + 'sub' => $this->endSubscriptTag(...), + 'br' => $this->breakTag(...), + 'p' => $this->breakTag(...), + 'h1' => $this->breakTag(...), + 'h2' => $this->breakTag(...), + 'h3' => $this->breakTag(...), + 'h4' => $this->breakTag(...), + 'h5' => $this->breakTag(...), + 'h6' => $this->breakTag(...), + ]; + } + } + + private function initialise(): void + { + $this->face = $this->size = $this->color = null; + $this->bold = $this->italic = $this->underline = $this->superscript = $this->subscript = $this->strikethrough = false; + + $this->stack = []; + + $this->stringData = ''; + } + + /** + * Parse HTML formatting and return the resulting RichText. + */ + public function toRichTextObject(string $html, bool $preserveWhiteSpace = false): RichText + { + $this->initialise(); + + // Create a new DOM object + $dom = new DOMDocument(); + // Load the HTML file into the DOM object + // Note the use of error suppression, because typically this will be an html fragment, so not fully valid markup + $prefix = ''; + @$dom->loadHTML($prefix . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + // Discard excess white space + $dom->preserveWhiteSpace = false; + + $this->richTextObject = new RichText(); + $this->preserveWhiteSpace = $preserveWhiteSpace; + $this->parseElements($dom); + $this->preserveWhiteSpace = false; + + // Clean any further spurious whitespace + $this->cleanWhitespace(); + + return $this->richTextObject; + } + + private function cleanWhitespace(): void + { + foreach ($this->richTextObject->getRichTextElements() as $key => $element) { + $text = $element->getText(); + // Trim any leading spaces on the first run + if ($key == 0) { + $text = ltrim($text); + } + // Trim any spaces immediately after a line break + $text = (string) preg_replace('/\n */mu', "\n", $text); + $element->setText($text); + } + } + + private function buildTextRun(): void + { + $text = $this->stringData; + if (trim($text) === '') { + return; + } + + $richtextRun = $this->richTextObject->createTextRun($this->stringData); + $font = $richtextRun->getFont(); + if ($font !== null) { + if ($this->face) { + $font->setName($this->face); + } + if ($this->size) { + $font->setSize($this->size); + } + if ($this->color) { + $font->setColor(new Color('ff' . $this->color)); + } + if ($this->bold) { + $font->setBold(true); + } + if ($this->italic) { + $font->setItalic(true); + } + if ($this->underline) { + $font->setUnderline(Font::UNDERLINE_SINGLE); + } + if ($this->superscript) { + $font->setSuperscript(true); + } + if ($this->subscript) { + $font->setSubscript(true); + } + if ($this->strikethrough) { + $font->setStrikethrough(true); + } + } + $this->stringData = ''; + } + + private function rgbToColour(string $rgbValue): string + { + preg_match_all('/\d+/', $rgbValue, $values); + foreach ($values[0] as &$value) { + $value = str_pad(dechex((int) $value), 2, '0', STR_PAD_LEFT); + } + + return implode('', $values[0]); + } + + public static function colourNameLookup(string $colorName): string + { + /** @var string[] */ + $temp = static::COLOUR_MAP; + + return $temp[$colorName] ?? ''; + } + + protected function startFontTag(DOMElement $tag): void + { + $attrs = $tag->attributes ?? []; + /** @var DOMAttr $attribute */ + foreach ($attrs as $attribute) { + $attributeName = strtolower($attribute->name); + $attributeName = preg_replace('/^html:/', '', $attributeName) ?? $attributeName; // in case from Xml spreadsheet + $attributeValue = $attribute->value; + + if ($attributeName === 'color') { + if (preg_match('/rgb\s*\(/', $attributeValue)) { + $this->$attributeName = $this->rgbToColour($attributeValue); + } elseif (str_starts_with(trim($attributeValue), '#')) { + $this->$attributeName = ltrim($attributeValue, '#'); + } else { + $this->$attributeName = static::colourNameLookup($attributeValue); + } + } elseif ($attributeName === 'face' || $attributeName === 'size') { + $this->$attributeName = $attributeValue; + } + } + } + + protected function endFontTag(): void + { + $this->face = $this->size = $this->color = null; + } + + protected function startBoldTag(): void + { + $this->bold = true; + } + + protected function endBoldTag(): void + { + $this->bold = false; + } + + protected function startItalicTag(): void + { + $this->italic = true; + } + + protected function endItalicTag(): void + { + $this->italic = false; + } + + protected function startUnderlineTag(): void + { + $this->underline = true; + } + + protected function endUnderlineTag(): void + { + $this->underline = false; + } + + protected function startSubscriptTag(): void + { + $this->subscript = true; + } + + protected function endSubscriptTag(): void + { + $this->subscript = false; + } + + protected function startSuperscriptTag(): void + { + $this->superscript = true; + } + + protected function endSuperscriptTag(): void + { + $this->superscript = false; + } + + protected function startStrikethruTag(): void + { + $this->strikethrough = true; + } + + protected function endStrikethruTag(): void + { + $this->strikethrough = false; + } + + public function breakTag(): void + { + $this->stringData .= "\n"; + } + + private function parseTextNode(DOMText $textNode): void + { + if ($this->preserveWhiteSpace) { + $domText = $textNode->nodeValue ?? ''; + } else { + $domText = (string) preg_replace( + '/\s+/u', + ' ', + str_replace(["\r", "\n"], ' ', $textNode->nodeValue ?? '') + ); + } + $this->stringData .= $domText; + $this->buildTextRun(); + } + + public function addStartTagCallback(string $tag, callable $callback): void + { + $this->startTagCallbacks[$tag] = $callback; + } + + public function addEndTagCallback(string $tag, callable $callback): void + { + $this->endTagCallbacks[$tag] = $callback; + } + + /** @param callable[] $callbacks */ + private function handleCallback(DOMElement $element, string $callbackTag, array $callbacks): void + { + if (isset($callbacks[$callbackTag])) { + $elementHandler = $callbacks[$callbackTag]; + call_user_func($elementHandler, $element, $this); + } + } + + private function parseElementNode(DOMElement $element): void + { + $callbackTag = strtolower($element->nodeName); + $this->stack[] = $callbackTag; + + $this->handleCallback($element, $callbackTag, $this->startTagCallbacks); + + $this->parseElements($element); + array_pop($this->stack); + + $this->handleCallback($element, $callbackTag, $this->endTagCallbacks); + } + + private function parseElements(DOMNode $element): void + { + foreach ($element->childNodes as $child) { + if ($child instanceof DOMText) { + $this->parseTextNode($child); + } elseif ($child instanceof DOMElement) { + $this->parseElementNode($child); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Sample.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Sample.php new file mode 100644 index 0000000..729d5d7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Sample.php @@ -0,0 +1,300 @@ +getScriptFilename() === 'index'; + } + + /** + * Return the page title. + */ + public function getPageTitle(): string + { + return $this->isIndex() ? 'PHPSpreadsheet' : $this->getScriptFilename(); + } + + /** + * Return the page heading. + */ + public function getPageHeading(): string + { + return $this->isIndex() ? '' : '

' . str_replace('_', ' ', $this->getScriptFilename()) . '

'; + } + + /** + * Returns an array of all known samples. + * + * @return string[][] [$name => $path] + */ + public function getSamples(): array + { + // Populate samples + $baseDir = realpath(__DIR__ . '/../../../samples'); + if ($baseDir === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException('realpath returned false'); + // @codeCoverageIgnoreEnd + } + $directory = new RecursiveDirectoryIterator($baseDir); + $iterator = new RecursiveIteratorIterator($directory); + $regex = new RegexIterator($iterator, '/^.+\.php$/', RecursiveRegexIterator::GET_MATCH); + + $files = []; + /** @var string[] $file */ + foreach ($regex as $file) { + $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); + $info = pathinfo($file); + $category = str_replace('_', ' ', $info['dirname'] ?? ''); + $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); + if (!in_array($category, ['.', 'bootstrap', 'templates']) && $name !== 'Header') { + if (!isset($files[$category])) { + $files[$category] = []; + } + $files[$category][$name] = $file; + } + } + + // Sort everything + ksort($files); + foreach ($files as &$f) { + asort($f); + } + + return $files; + } + + /** + * Write documents. + * + * @param string[] $writers + */ + public function write(Spreadsheet $spreadsheet, string $filename, array $writers = ['Xlsx', 'Xls'], bool $withCharts = false, ?callable $writerCallback = null, bool $resetActiveSheet = true): void + { + // Set active sheet index to the first sheet, so Excel opens this as the first sheet + if ($resetActiveSheet) { + $spreadsheet->setActiveSheetIndex(0); + } + + // Write documents + foreach ($writers as $writerType) { + $path = $this->getFilename($filename, mb_strtolower($writerType)); + if (preg_match('/(mpdf|tcpdf)$/', $path)) { + $path .= '.pdf'; + } + $writer = IOFactory::createWriter($spreadsheet, $writerType); + $writer->setIncludeCharts($withCharts); + if ($writerCallback !== null) { + $writerCallback($writer); + } + $callStartTime = microtime(true); + $writer->save($path); + $this->logWrite($writer, $path, $callStartTime); + if ($this->isCli() === false) { + // @codeCoverageIgnoreStart + echo 'Download ' . basename($path) . '
'; + // @codeCoverageIgnoreEnd + } + } + + $this->logEndingNotes(); + } + + protected function isDirOrMkdir(string $folder): bool + { + return \is_dir($folder) || \mkdir($folder); + } + + /** + * Returns the temporary directory and make sure it exists. + */ + public function getTemporaryFolder(): string + { + $tempFolder = sys_get_temp_dir() . '/phpspreadsheet'; + if (!$this->isDirOrMkdir($tempFolder)) { + throw new RuntimeException(sprintf('Directory "%s" was not created', $tempFolder)); + } + + return $tempFolder; + } + + /** + * Returns the filename that should be used for sample output. + */ + public function getFilename(string $filename, string $extension = 'xlsx'): string + { + $originalExtension = pathinfo($filename, PATHINFO_EXTENSION); + + return $this->getTemporaryFolder() . '/' . str_replace('.' . $originalExtension, '.' . $extension, basename($filename)); + } + + /** + * Return a random temporary file name. + */ + public function getTemporaryFilename(string $extension = 'xlsx'): string + { + $temporaryFilename = tempnam($this->getTemporaryFolder(), 'phpspreadsheet-'); + if ($temporaryFilename === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException('tempnam returned false'); + // @codeCoverageIgnoreEnd + } + unlink($temporaryFilename); + + return $temporaryFilename . '.' . $extension; + } + + public function log(mixed $message): void + { + $eol = $this->isCli() ? PHP_EOL : '
'; + echo ($this->isCli() ? date('H:i:s ') : '') . StringHelper::convertToString($message) . $eol; + } + + /** + * Render chart as part of running chart samples in browser. + * Charts are not rendered in unit tests, which are command line. + * + * @codeCoverageIgnore + */ + public function renderChart(Chart $chart, string $fileName, ?Spreadsheet $spreadsheet = null): void + { + if ($this->isCli() === true) { + return; + } + Settings::setChartRenderer(MtJpGraphRenderer::class); + + $fileName = $this->getFilename($fileName, 'png'); + $title = $chart->getTitle(); + $caption = null; + if ($title !== null) { + $calculatedTitle = $title->getCalculatedTitle($spreadsheet); + if ($calculatedTitle !== null) { + $caption = $title->getCaption(); + $title->setCaption($calculatedTitle); + } + } + + try { + $chart->render($fileName); + $this->log('Rendered image: ' . $fileName); + $imageData = @file_get_contents($fileName); + if ($imageData !== false) { + echo '
'; + } else { + $this->log('Unable to open chart' . PHP_EOL); + } + } catch (Throwable $e) { + $this->log('Error rendering chart: ' . $e->getMessage() . PHP_EOL); + } + if (isset($title, $caption)) { + $title->setCaption($caption); + } + Settings::unsetChartRenderer(); + } + + public function titles(string $category, string $functionName, ?string $description = null): void + { + $this->log(sprintf('%s Functions:', $category)); + $description === null + ? $this->log(sprintf('Function: %s()', rtrim($functionName, '()'))) + : $this->log(sprintf('Function: %s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + } + + /** @param mixed[][] $matrix */ + public function displayGrid(array $matrix): void + { + $renderer = new TextGrid($matrix, $this->isCli()); + echo $renderer->render(); + } + + public function logCalculationResult( + Worksheet $worksheet, + string $functionName, + string $formulaCell, + ?string $descriptionCell = null + ): void { + if ($descriptionCell !== null) { + $this->log($worksheet->getCell($descriptionCell)->getValueString()); + } + $this->log($worksheet->getCell($formulaCell)->getValueString()); + $this->log(sprintf('%s() Result is ', $functionName) . $worksheet->getCell($formulaCell)->getCalculatedValueString()); + } + + /** + * Log ending notes. + */ + public function logEndingNotes(): void + { + // Do not show execution time for index + $this->log('Peak memory usage: ' . (memory_get_peak_usage(true) / 1024 / 1024) . 'MB'); + } + + /** + * Log a line about the write operation. + */ + public function logWrite(IWriter $writer, string $path, float $callStartTime): void + { + $callEndTime = microtime(true); + $callTime = $callEndTime - $callStartTime; + $reflection = new ReflectionClass($writer); + $format = $reflection->getShortName(); + + $codePath = $this->isCli() ? $path : "$path"; + $message = "Write {$format} format to {$codePath} in " . sprintf('%.4f', $callTime) . ' seconds'; + + $this->log($message); + } + + /** + * Log a line about the read operation. + */ + public function logRead(string $format, string $path, float $callStartTime): void + { + $callEndTime = microtime(true); + $callTime = $callEndTime - $callStartTime; + $message = "Read {$format} format from {$path} in " . sprintf('%.4f', $callTime) . ' seconds'; + + $this->log($message); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Size.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Size.php new file mode 100644 index 0000000..c3e4e45 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/Size.php @@ -0,0 +1,45 @@ +\d*\.?\d+)(?Ppt|px|em)?$/i'; + + protected bool $valid = false; + + protected string $size = ''; + + protected string $unit = ''; + + public function __construct(string $size) + { + if (1 === preg_match(self::REGEXP_SIZE_VALIDATION, $size, $matches)) { + $this->valid = true; + $this->size = $matches['size']; + $this->unit = $matches['unit'] ?? 'pt'; + } + } + + public function valid(): bool + { + return $this->valid; + } + + public function size(): string + { + return $this->size; + } + + public function unit(): string + { + return $this->unit; + } + + public function __toString(): string + { + return $this->size . $this->unit; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/TextGrid.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/TextGrid.php new file mode 100644 index 0000000..9c64c61 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Helper/TextGrid.php @@ -0,0 +1,178 @@ +rows = array_keys($matrix); + $this->columns = array_keys($matrix[$this->rows[0]]); + + $matrix = array_values($matrix); + array_walk( + $matrix, + function (&$row): void { + $row = array_values($row); + } + ); + + $this->matrix = $matrix; + $this->isCli = $isCli; + $this->rowDividers = $rowDividers; + $this->rowHeaders = $rowHeaders; + $this->columnHeaders = $columnHeaders; + } + + public function render(): string + { + $this->gridDisplay = $this->isCli ? '' : ('
' . PHP_EOL);
+
+        if (!empty($this->rows)) {
+            $maxRow = max($this->rows);
+            $maxRowLength = strlen((string) $maxRow) + 1;
+            $columnWidths = $this->getColumnWidths();
+
+            $this->renderColumnHeader($maxRowLength, $columnWidths);
+            $this->renderRows($maxRowLength, $columnWidths);
+            if (!$this->rowDividers) {
+                $this->renderFooter($maxRowLength, $columnWidths);
+            }
+        }
+
+        $this->gridDisplay .= $this->isCli ? '' : '
'; + + return $this->gridDisplay; + } + + /** @param int[] $columnWidths */ + private function renderRows(int $maxRowLength, array $columnWidths): void + { + foreach ($this->matrix as $row => $rowData) { + if ($this->rowHeaders) { + $this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' '; + } + $this->renderCells($rowData, $columnWidths); + $this->gridDisplay .= '|' . PHP_EOL; + if ($this->rowDividers) { + $this->renderFooter($maxRowLength, $columnWidths); + } + } + } + + /** + * @param mixed[] $rowData + * @param int[] $columnWidths + */ + private function renderCells(array $rowData, array $columnWidths): void + { + foreach ($rowData as $column => $cell) { + $valueForLength = $this->getString($cell); + $displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength); + $this->gridDisplay .= '| '; + $this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1); + } + } + + /** @param int[] $columnWidths */ + private function renderColumnHeader(int $maxRowLength, array &$columnWidths): void + { + if (!$this->columnHeaders) { + $this->renderFooter($maxRowLength, $columnWidths); + + return; + } + foreach ($this->columns as $column => $reference) { + /** @var string $reference */ + $columnWidths[$column] = max($columnWidths[$column], $this->strlen($reference)); + } + if ($this->rowHeaders) { + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + } + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1); + } + $this->gridDisplay .= '+' . PHP_EOL; + + if ($this->rowHeaders) { + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + } + foreach ($this->columns as $column => $reference) { + /** @var scalar $reference */ + $this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' '); + } + $this->gridDisplay .= '|' . PHP_EOL; + + $this->renderFooter($maxRowLength, $columnWidths); + } + + /** @param int[] $columnWidths */ + private function renderFooter(int $maxRowLength, array $columnWidths): void + { + if ($this->rowHeaders) { + $this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1); + } + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-'; + $this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-'); + } + $this->gridDisplay .= '+' . PHP_EOL; + } + + /** @return int[] */ + private function getColumnWidths(): array + { + $columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix); + $columnWidths = []; + for ($column = 0; $column < $columnCount; ++$column) { + $columnWidths[] = $this->getColumnWidth(array_column($this->matrix, $column)); + } + + return $columnWidths; + } + + /** @param mixed[] $columnData */ + private function getColumnWidth(array $columnData): int + { + $columnWidth = 0; + $columnData = array_values($columnData); + + foreach ($columnData as $columnValue) { + $columnWidth = max($columnWidth, $this->strlen($this->getString($columnValue))); + } + + return $columnWidth; + } + + protected function getString(mixed $value): string + { + return StringHelper::convertToString($value, convertBool: true); + } + + protected function strlen(string $value): int + { + return mb_strlen($value); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/IComparable.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/IComparable.php new file mode 100644 index 0000000..64b451e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/IComparable.php @@ -0,0 +1,13 @@ +> */ + private static array $readers = [ + self::READER_XLSX => Reader\Xlsx::class, + self::READER_XLS => Reader\Xls::class, + self::READER_XML => Reader\Xml::class, + self::READER_ODS => Reader\Ods::class, + self::READER_SLK => Reader\Slk::class, + self::READER_GNUMERIC => Reader\Gnumeric::class, + self::READER_HTML => Reader\Html::class, + self::READER_CSV => Reader\Csv::class, + ]; + + /** @var array> */ + private static array $writers = [ + self::WRITER_XLS => Writer\Xls::class, + self::WRITER_XLSX => Writer\Xlsx::class, + self::WRITER_ODS => Writer\Ods::class, + self::WRITER_CSV => Writer\Csv::class, + self::WRITER_HTML => Writer\Html::class, + 'Tcpdf' => Writer\Pdf\Tcpdf::class, + 'Dompdf' => Writer\Pdf\Dompdf::class, + 'Mpdf' => Writer\Pdf\Mpdf::class, + ]; + + /** + * Create Writer\IWriter. + */ + public static function createWriter(Spreadsheet $spreadsheet, string $writerType): IWriter + { + /** @var class-string */ + $className = $writerType; + if (!in_array($writerType, self::$writers, true)) { + if (!isset(self::$writers[$writerType])) { + throw new Writer\Exception("No writer found for type $writerType"); + } + + // Instantiate writer + $className = self::$writers[$writerType]; + } + + return new $className($spreadsheet); + } + + /** + * Create IReader. + */ + public static function createReader(string $readerType): IReader + { + /** @var class-string */ + $className = $readerType; + if (!in_array($readerType, self::$readers, true)) { + if (!isset(self::$readers[$readerType])) { + throw new Reader\Exception("No reader found for type $readerType"); + } + + // Instantiate reader + $className = self::$readers[$readerType]; + } + + return new $className(); + } + + /** + * Loads Spreadsheet from file using automatic Reader\IReader resolution. + * + * @param string $filename The name of the spreadsheet file + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file. + * IReader::READ_DATA_ONLY - Read cell values only, not formatting or merge structure. + * IReader::IGNORE_EMPTY_CELLS - Don't load empty cells into the model. + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. + */ + public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet + { + $reader = self::createReaderForFile($filename, $readers); + + return $reader->load($filename, $flags); + } + + /** + * Identify file type using automatic IReader resolution. + * + * @param string[] $readers + */ + public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false): string + { + $reader = self::createReaderForFile($filename, $readers); + $className = $reader::class; + if ($fullClassName) { + return $className; + } + $classType = explode('\\', $className); + + return array_pop($classType); + } + + /** + * Create Reader\IReader for file using automatic IReader resolution. + * + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. + */ + public static function createReaderForFile(string $filename, ?array $readers = null): IReader + { + File::assertFile($filename); + + $testReaders = self::$readers; + if ($readers !== null) { + $readers = array_map('strtoupper', $readers); + $testReaders = array_filter( + self::$readers, + fn (string $readerType): bool => in_array(strtoupper($readerType), $readers, true), + ARRAY_FILTER_USE_KEY + ); + } + + // First, lucky guess by inspecting file extension + $guessedReader = self::getReaderTypeFromExtension($filename); + if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) { + $reader = self::createReader($guessedReader); + + // Let's see if we are lucky + if ($reader->canRead($filename)) { + return $reader; + } + } + + // If we reach here then "lucky guess" didn't give any result + // Try walking through all the options in self::$readers (or the selected subset) + foreach ($testReaders as $readerType => $class) { + // Ignore our original guess, we know that won't work + if ($readerType !== $guessedReader) { + $reader = self::createReader($readerType); + if ($reader->canRead($filename)) { + return $reader; + } + } + } + + throw new Reader\Exception('Unable to identify a reader for this file'); + } + + /** + * Guess a reader type from the file extension, if any. + */ + private static function getReaderTypeFromExtension(string $filename): ?string + { + $pathinfo = pathinfo($filename); + if (!isset($pathinfo['extension'])) { + return null; + } + + return match (strtolower($pathinfo['extension'])) { + // Excel (OfficeOpenXML) Spreadsheet + 'xlsx', + // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded) + 'xlsm', + // Excel (OfficeOpenXML) Template + 'xltx', + // Excel (OfficeOpenXML) Macro Template (macros will be discarded) + 'xltm' => 'Xlsx', + // Excel (BIFF) Spreadsheet + 'xls', + // Excel (BIFF) Template + 'xlt' => 'Xls', + // Open/Libre Offic Calc + 'ods', + // Open/Libre Offic Calc Template + 'ots' => 'Ods', + 'slk' => 'Slk', + // Excel 2003 SpreadSheetML + 'xml' => 'Xml', + 'gnumeric' => 'Gnumeric', + 'htm', 'html' => 'Html', + // Do nothing + // We must not try to use CSV reader since it loads + // all files including Excel files etc. + 'csv' => null, + default => null, + }; + } + + /** + * Register a writer with its type and class name. + * + * @param class-string $writerClass + */ + public static function registerWriter(string $writerType, string $writerClass): void + { + // We want phpstan to validate caller, but still need this test + if (!is_a($writerClass, IWriter::class, true)) { //* @phpstan-ignore-line + throw new Writer\Exception('Registered writers must implement ' . IWriter::class); + } + + self::$writers[$writerType] = $writerClass; + } + + /** + * Register a reader with its type and class name. + * + * @param class-string $readerClass + */ + public static function registerReader(string $readerType, string $readerClass): void + { + // We want phpstan to validate caller, but still need this test + if (!is_a($readerClass, IReader::class, true)) { //* @phpstan-ignore-line + throw new Reader\Exception('Registered readers must implement ' . IReader::class); + } + + self::$readers[$readerType] = $readerClass; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/NamedFormula.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/NamedFormula.php new file mode 100644 index 0000000..500151f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/NamedFormula.php @@ -0,0 +1,45 @@ +value; + } + + /** + * Set the formula value. + */ + public function setFormula(string $formula): self + { + if (!empty($formula)) { + $this->value = $formula; + } + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/NamedRange.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/NamedRange.php new file mode 100644 index 0000000..c7fb853 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/NamedRange.php @@ -0,0 +1,56 @@ +value; + } + + /** + * Set the range value. + */ + public function setRange(string $range): self + { + if (!empty($range)) { + $this->value = $range; + } + + return $this; + } + + /** @return string[] */ + public function getCellsInRange(): array + { + $range = $this->value; + if (str_starts_with($range, '=')) { + $range = substr($range, 1); + } + + return Coordinate::extractAllCellReferencesInRange($range); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/BaseReader.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/BaseReader.php new file mode 100644 index 0000000..c4184bd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/BaseReader.php @@ -0,0 +1,319 @@ +readFilter = new DefaultReadFilter(); + } + + public function getReadDataOnly(): bool + { + return $this->readDataOnly; + } + + public function setReadDataOnly(bool $readCellValuesOnly): self + { + $this->readDataOnly = $readCellValuesOnly; + + return $this; + } + + public function getReadEmptyCells(): bool + { + return $this->readEmptyCells; + } + + public function setReadEmptyCells(bool $readEmptyCells): self + { + $this->readEmptyCells = $readEmptyCells; + + return $this; + } + + public function getIgnoreRowsWithNoCells(): bool + { + return $this->ignoreRowsWithNoCells; + } + + public function setIgnoreRowsWithNoCells(bool $ignoreRowsWithNoCells): self + { + $this->ignoreRowsWithNoCells = $ignoreRowsWithNoCells; + + return $this; + } + + public function getIncludeCharts(): bool + { + return $this->includeCharts; + } + + public function setIncludeCharts(bool $includeCharts): self + { + $this->includeCharts = $includeCharts; + + return $this; + } + + /** @return null|string[] */ + public function getLoadSheetsOnly(): ?array + { + return $this->loadSheetsOnly; + } + + /** @param null|string|string[] $sheetList */ + public function setLoadSheetsOnly(string|array|null $sheetList): self + { + if ($sheetList === null) { + return $this->setLoadAllSheets(); + } + + $this->loadSheetsOnly = is_array($sheetList) ? $sheetList : [$sheetList]; + + return $this; + } + + public function setLoadAllSheets(): self + { + $this->loadSheetsOnly = null; + + return $this; + } + + public function getReadFilter(): IReadFilter + { + return $this->readFilter; + } + + public function setReadFilter(IReadFilter $readFilter): self + { + $this->readFilter = $readFilter; + + return $this; + } + + /** + * Allow external images. Use with caution. + * Improper specification of these within a spreadsheet + * can subject the caller to security exploits. + */ + public function setAllowExternalImages(bool $allowExternalImages): self + { + $this->allowExternalImages = $allowExternalImages; + + return $this; + } + + public function getAllowExternalImages(): bool + { + return $this->allowExternalImages; + } + + /** + * Create a blank sheet if none are read, + * possibly due to a typo when using LoadSheetsOnly. + */ + public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self + { + $this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead; + + return $this; + } + + public function getSecurityScanner(): ?XmlScanner + { + return $this->securityScanner; + } + + public function getSecurityScannerOrThrow(): XmlScanner + { + if ($this->securityScanner === null) { + throw new ReaderException('Security scanner is unexpectedly null'); + } + + return $this->securityScanner; + } + + protected function processFlags(int $flags): void + { + if (((bool) ($flags & self::LOAD_WITH_CHARTS)) === true) { + $this->setIncludeCharts(true); + } + if (((bool) ($flags & self::READ_DATA_ONLY)) === true) { + $this->setReadDataOnly(true); + } + if (((bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) { + $this->setReadEmptyCells(false); + } + if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) { + $this->setIgnoreRowsWithNoCells(true); + } + if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) { + $this->setAllowExternalImages(true); + } + if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) { + $this->setAllowExternalImages(false); + } + if (((bool) ($flags & self::CREATE_BLANK_SHEET_IF_NONE_READ)) === true) { + $this->setCreateBlankSheetIfNoneRead(true); + } + } + + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + throw new PhpSpreadsheetException('Reader classes must implement their own loadSpreadsheetFromFile() method'); + } + + /** + * Loads Spreadsheet from file. + * + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file + */ + public function load(string $filename, int $flags = 0): Spreadsheet + { + $this->processFlags($flags); + + try { + return $this->loadSpreadsheetFromFile($filename); + } catch (ReaderException $e) { + throw $e; + } + } + + /** + * Open file for reading. + */ + protected function openFile(string $filename): void + { + $fileHandle = false; + if ($filename) { + File::assertFile($filename); + + // Open file + $fileHandle = fopen($filename, 'rb'); + } + if ($fileHandle === false) { + throw new ReaderException('Could not open file ' . $filename . ' for reading.'); + } + + $this->fileHandle = $fileHandle; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + throw new PhpSpreadsheetException('Reader classes must implement their own listWorksheetInfo() method'); + } + + /** + * Returns names of the worksheets from a file, + * possibly without parsing the whole file to a Spreadsheet object. + * Readers will often have a more efficient method with which + * they can override this method. + * + * @return string[] + */ + public function listWorksheetNames(string $filename): array + { + $returnArray = []; + $info = $this->listWorksheetInfo($filename); + foreach ($info as $infoArray) { + $returnArray[] = $infoArray['worksheetName']; + } + + return $returnArray; + } + + public function getValueBinder(): ?IValueBinder + { + return $this->valueBinder; + } + + public function setValueBinder(?IValueBinder $valueBinder): self + { + $this->valueBinder = $valueBinder; + + return $this; + } + + protected function newSpreadsheet(): Spreadsheet + { + return new Spreadsheet(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Csv.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Csv.php new file mode 100644 index 0000000..55fe08a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Csv.php @@ -0,0 +1,762 @@ +inputEncoding = $encoding; + + return $this; + } + + public function getInputEncoding(): string + { + return $this->inputEncoding; + } + + public function setFallbackEncoding(string $fallbackEncoding): self + { + $this->fallbackEncoding = $fallbackEncoding; + + return $this; + } + + public function getFallbackEncoding(): string + { + return $this->fallbackEncoding; + } + + /** + * Move filepointer past any BOM marker. + */ + protected function skipBOM(): void + { + rewind($this->fileHandle); + + if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) { + rewind($this->fileHandle); + } + } + + /** + * Identify any separator that is explicitly set in the file. + */ + protected function checkSeparator(): void + { + $line = fgets($this->fileHandle); + if ($line === false) { + return; + } + + if ((strlen(trim($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) { + $this->delimiter = substr($line, 4, 1); + + return; + } + + $this->skipBOM(); + } + + /** + * Infer the separator if it isn't explicitly set in the file or specified by the user. + */ + protected function inferSeparator(): void + { + $temp = $this->delimiter; + if ($temp !== null) { + return; + } + + $inferenceEngine = new Delimiter($this->fileHandle, $this->getEscapeCharacter(), $this->enclosure); + + // If number of lines is 0, nothing to infer : fall back to the default + if ($inferenceEngine->linesCounted() === 0) { + $this->delimiter = $inferenceEngine->getDefaultDelimiter(); + $this->skipBOM(); + + return; + } + + $this->delimiter = $inferenceEngine->infer(); + + // If no delimiter could be detected, fall back to the default + if ($this->delimiter === null) { + $this->delimiter = $inferenceEngine->getDefaultDelimiter(); + } + + $this->skipBOM(); + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + // Open file + $this->openFileOrMemory($filename); + $fileHandle = $this->fileHandle; + + // Skip BOM, if any + $this->skipBOM(); + $this->checkSeparator(); + $this->inferSeparator(); + + $worksheetInfo = [ + [ + 'worksheetName' => 'Worksheet', + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + ], + ]; + $delimiter = $this->delimiter ?? ''; + + // Loop through each line of the file in turn + $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); + while (is_array($rowData)) { + ++$worksheetInfo[0]['totalRows']; + $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1); + $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); + } + + $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); + $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1; + $worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE; + + // Close file + fclose($fileHandle); + + return $worksheetInfo; + } + + /** + * Loads Spreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Loads Spreadsheet from string. + */ + public function loadSpreadsheetFromString(string $contents): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + + // Load into this instance + return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true); + } + + private function openFileOrMemory(string $filename): void + { + // Open file + $fhandle = $this->canRead($filename); + if (!$fhandle) { + throw new ReaderException($filename . ' is an Invalid Spreadsheet file.'); + } + if ($this->inputEncoding === 'UTF-8') { + $encoding = self::guessEncodingBom($filename); + if ($encoding !== '') { + $this->inputEncoding = $encoding; + } + } + if ($this->inputEncoding === self::GUESS_ENCODING) { + $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding); + } + $this->openFile($filename); + if ($this->inputEncoding !== 'UTF-8') { + fclose($this->fileHandle); + $entireFile = file_get_contents($filename); + $fileHandle = fopen('php://memory', 'r+b'); + if ($fileHandle !== false && $entireFile !== false) { + $this->fileHandle = $fileHandle; + $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding); + fwrite($this->fileHandle, $data); + $this->skipBOM(); + } + } + } + + public function setTestAutoDetect(bool $value): self + { + $this->testAutodetect = $value; + + return $this; + } + + private function setAutoDetect(?string $value, int $version = PHP_VERSION_ID): ?string + { + $retVal = null; + if ($value !== null && $this->testAutodetect && $version < 90000) { + $retVal2 = @ini_set('auto_detect_line_endings', $value); + if (is_string($retVal2)) { + $retVal = $retVal2; + } + } + + return $retVal; + } + + public function castFormattedNumberToNumeric( + bool $castFormattedNumberToNumeric, + bool $preserveNumericFormatting = false + ): void { + $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric; + $this->preserveNumericFormatting = $preserveNumericFormatting; + } + + /** + * Open data uri for reading. + */ + private function openDataUri(string $filename): void + { + $fileHandle = fopen($filename, 'rb'); + if ($fileHandle === false) { + // @codeCoverageIgnoreStart + throw new ReaderException('Could not open file ' . $filename . ' for reading.'); + // @codeCoverageIgnoreEnd + } + + $this->fileHandle = $fileHandle; + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + return $this->loadStringOrFile($filename, $spreadsheet, false); + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + */ + private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet + { + // Deprecated in Php8.1 + $iniset = $this->setAutoDetect('1'); + + try { + $this->loadStringOrFile2($filename, $spreadsheet, $dataUri); + $this->setAutoDetect($iniset); + } catch (Throwable $e) { + $this->setAutoDetect($iniset); + + throw $e; + } + + return $spreadsheet; + } + + private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void + { + + // Open file + if ($dataUri) { + $this->openDataUri($filename); + } else { + $this->openFileOrMemory($filename); + } + $fileHandle = $this->fileHandle; + + // Skip BOM, if any + $this->skipBOM(); + $this->checkSeparator(); + $this->inferSeparator(); + + // Create new PhpSpreadsheet object + while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { + $spreadsheet->createSheet(); + } + $sheet = $spreadsheet->setActiveSheetIndex($this->sheetIndex); + if ($this->sheetNameIsFileName) { + $sheet->setTitle(substr(basename($filename, '.csv'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH)); + } + + // Set our starting row based on whether we're in contiguous mode or not + $currentRow = 1; + $outRow = 0; + + // Loop through each line of the file in turn + $delimiter = $this->delimiter ?? ''; + $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); + $valueBinder = $this->valueBinder ?? Cell::getValueBinder(); + $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion(); + $this->getTrue = Calculation::getTRUE(); + $this->getFalse = Calculation::getFALSE(); + $this->thousandsSeparator = StringHelper::getThousandsSeparator(); + $this->decimalSeparator = StringHelper::getDecimalSeparator(); + while (is_array($rowData)) { + $noOutputYet = true; + $columnLetter = 'A'; + foreach ($rowData as $rowDatum) { + if ($preserveBooleanString) { + $rowDatum = $rowDatum ?? ''; + } else { + $this->convertBoolean($rowDatum); + } + $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : ''; + if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { + if ($this->contiguous) { + if ($noOutputYet) { + $noOutputYet = false; + ++$outRow; + } + } else { + $outRow = $currentRow; + } + // Set basic styling for the value (Note that this could be overloaded by styling in a value binder) + if ($numberFormatMask !== '') { + $sheet->getStyle($columnLetter . $outRow) + ->getNumberFormat() + ->setFormatCode($numberFormatMask); + } + // Set cell value + $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); + } + StringHelper::stringIncrement($columnLetter); + } + $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); + ++$currentRow; + } + + // Close file + fclose($fileHandle); + } + + /** + * Convert string true/false to boolean, and null to null-string. + */ + private function convertBoolean(mixed &$rowDatum): void + { + if (is_string($rowDatum)) { + if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { + $rowDatum = true; + } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { + $rowDatum = false; + } + } else { + $rowDatum = $rowDatum ?? ''; + } + } + + /** + * Convert numeric strings to int or float values. + */ + private function convertFormattedNumber(mixed &$rowDatum): string + { + $numberFormatMask = ''; + if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) { + $numeric = str_replace( + [$this->thousandsSeparator, $this->decimalSeparator], + ['', '.'], + $rowDatum + ); + + if (is_numeric($numeric)) { + $decimalPos = strpos($rowDatum, $this->decimalSeparator); + if ($this->preserveNumericFormatting === true) { + $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator)) + ? '#,##0' : '0'; + if ($decimalPos !== false) { + $decimals = strlen($rowDatum) - $decimalPos - 1; + $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6)); + } + } + + $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric; + } + } + + return $numberFormatMask; + } + + public function getDelimiter(): ?string + { + return $this->delimiter; + } + + public function setDelimiter(?string $delimiter): self + { + $this->delimiter = $delimiter; + + return $this; + } + + public function getEnclosure(): string + { + return $this->enclosure; + } + + public function setEnclosure(string $enclosure): self + { + if ($enclosure == '') { + $enclosure = '"'; + } + $this->enclosure = $enclosure; + + return $this; + } + + public function getSheetIndex(): int + { + return $this->sheetIndex; + } + + public function setSheetIndex(int $indexValue): self + { + $this->sheetIndex = $indexValue; + + return $this; + } + + public function setContiguous(bool $contiguous): self + { + $this->contiguous = $contiguous; + + return $this; + } + + public function getContiguous(): bool + { + return $this->contiguous; + } + + /** + * Php9 intends to drop support for this parameter in fgetcsv. + * Not yet ready to mark deprecated in order to give users + * a migration path. + */ + public function setEscapeCharacter(string $escapeCharacter, int $version = PHP_VERSION_ID): self + { + if ($version >= 90000 && $escapeCharacter !== '') { + throw new ReaderException('Escape character must be null string for Php9+'); + } + + $this->escapeCharacter = $escapeCharacter; + + return $this; + } + + public function getEscapeCharacter(int $version = PHP_VERSION_ID): string + { + return $this->escapeCharacter ?? self::getDefaultEscapeCharacter($version); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + // Check if file exists + try { + $this->openFile($filename); + } catch (ReaderException) { + return false; + } + + fclose($this->fileHandle); + + // Trust file extension if any + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if (in_array($extension, ['csv', 'tsv'])) { + return true; + } + + // Attempt to guess mimetype + $type = mime_content_type($filename); + $supportedTypes = [ + 'application/csv', + 'text/csv', + 'text/plain', + 'inode/x-empty', + 'application/x-empty', // has now replaced previous + 'text/html', + ]; + + return in_array($type, $supportedTypes, true); + } + + private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void + { + if ($encoding === '') { + $pos = strpos($contents, $compare); + if ($pos !== false && $pos % strlen($compare) === 0) { + $encoding = $setEncoding; + } + } + } + + private static function guessEncodingNoBom(string $filename): string + { + $encoding = ''; + $contents = (string) file_get_contents($filename); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE'); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE'); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE'); + self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE'); + if ($encoding === '' && preg_match('//u', $contents) === 1) { + $encoding = 'UTF-8'; + } + + return $encoding; + } + + private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void + { + if ($encoding === '') { + if (str_starts_with($first4, $compare)) { + $encoding = $setEncoding; + } + } + } + + public static function guessEncodingBom(string $filename, ?string $convertString = null): string + { + $encoding = ''; + $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4); + self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); + self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); + + return $encoding; + } + + public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string + { + $encoding = self::guessEncodingBom($filename); + if ($encoding === '') { + $encoding = self::guessEncodingNoBom($filename); + } + + return ($encoding === '') ? $dflt : $encoding; + } + + public function setPreserveNullString(bool $value): self + { + $this->preserveNullString = $value; + + return $this; + } + + public function getPreserveNullString(): bool + { + return $this->preserveNullString; + } + + public function setSheetNameIsFileName(bool $sheetNameIsFileName): self + { + $this->sheetNameIsFileName = $sheetNameIsFileName; + + return $this; + } + + /** + * Php8.4 deprecates use of anything other than null string + * as escape Character. + * + * @param resource $stream + * @param null|int<0, max> $length + * + * @return array|false + */ + private static function getCsv( + $stream, + ?int $length = null, + string $separator = ',', + string $enclosure = '"', + ?string $escape = null, + int $version = PHP_VERSION_ID + ): array|false { + $escape = $escape ?? self::getDefaultEscapeCharacter(); + if ($version >= 80400 && $escape !== '') { + return @fgetcsv($stream, $length, $separator, $enclosure, $escape); + } + + return fgetcsv($stream, $length, $separator, $enclosure, $escape); + } + + public static function affectedByPhp9( + string $filename, + string $inputEncoding = 'UTF-8', + ?string $delimiter = null, + string $enclosure = '"', + string $escapeCharacter = '\\', + int $version = PHP_VERSION_ID + ): bool { + if ($version < 70400 || $version >= 90000) { + throw new ReaderException('Function valid only for Php7.4 or Php8'); + } + $reader1 = new self(); + $reader1->setInputEncoding($inputEncoding) + ->setTestAutoDetect(true) + ->setEscapeCharacter($escapeCharacter) + ->setDelimiter($delimiter) + ->setEnclosure($enclosure); + $spreadsheet1 = $reader1->load($filename); + $sheet1 = $spreadsheet1->getActiveSheet(); + $array1 = $sheet1->toArray(null, false, false); + $spreadsheet1->disconnectWorksheets(); + + $reader2 = new self(); + $reader2->setInputEncoding($inputEncoding) + ->setTestAutoDetect(false) + ->setEscapeCharacter('') + ->setDelimiter($delimiter) + ->setEnclosure($enclosure); + $spreadsheet2 = $reader2->load($filename); + $sheet2 = $spreadsheet2->getActiveSheet(); + $array2 = $sheet2->toArray(null, false, false); + $spreadsheet2->disconnectWorksheets(); + + return $array1 !== $array2; + } + + /** + * The character that will be supplied to fgetcsv + * when escapeCharacter is null. + * It is anticipated that it will conditionally be set + * to null-string for Php9 and above. + */ + private static function getDefaultEscapeCharacter(int $version = PHP_VERSION_ID): string + { + return $version < 90000 ? '\\' : ''; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Csv/Delimiter.php new file mode 100644 index 0000000..a3370e0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Csv/Delimiter.php @@ -0,0 +1,144 @@ + */ + protected array $counts = []; + + protected int $numberLines = 0; + + protected ?string $delimiter = null; + + /** + * @param resource $fileHandle + */ + public function __construct($fileHandle, string $escapeCharacter, string $enclosure) + { + $this->fileHandle = $fileHandle; + $this->escapeCharacter = $escapeCharacter; + $this->enclosure = $enclosure; + + $this->countPotentialDelimiters(); + } + + public function getDefaultDelimiter(): string + { + return self::POTENTIAL_DELIMITERS[0]; + } + + public function linesCounted(): int + { + return $this->numberLines; + } + + protected function countPotentialDelimiters(): void + { + $this->counts = array_fill_keys(self::POTENTIAL_DELIMITERS, []); + $delimiterKeys = array_flip(self::POTENTIAL_DELIMITERS); + + // Count how many times each of the potential delimiters appears in each line + $this->numberLines = 0; + while (($line = $this->getNextLine()) !== false && (++$this->numberLines < 1000)) { + $this->countDelimiterValues($line, $delimiterKeys); + } + } + + /** @param array $delimiterKeys */ + protected function countDelimiterValues(string $line, array $delimiterKeys): void + { + $splitString = mb_str_split($line, 1, 'UTF-8'); + $distribution = array_count_values($splitString); + $countLine = array_intersect_key($distribution, $delimiterKeys); + + foreach (self::POTENTIAL_DELIMITERS as $delimiter) { + $this->counts[$delimiter][] = $countLine[$delimiter] ?? 0; + } + } + + public function infer(): ?string + { + // Calculate the mean square deviations for each delimiter + // (ignoring delimiters that haven't been found consistently) + $meanSquareDeviations = []; + $middleIdx = (int) floor(($this->numberLines - 1) / 2); + + foreach (self::POTENTIAL_DELIMITERS as $delimiter) { + $series = $this->counts[$delimiter]; + sort($series); + + $median = ($this->numberLines % 2) + ? $series[$middleIdx] + : ($series[$middleIdx] + $series[$middleIdx + 1]) / 2; + + if ($median === 0) { + continue; + } + + $meanSquareDeviations[$delimiter] = array_reduce( + $series, + fn ($sum, $value): int|float => $sum + ($value - $median) ** 2 + ) / count($series); + } + + // ... and pick the delimiter with the smallest mean square deviation + // (in case of ties, the order in potentialDelimiters is respected) + $min = INF; + foreach (self::POTENTIAL_DELIMITERS as $delimiter) { + if (!isset($meanSquareDeviations[$delimiter])) { + continue; + } + + if ($meanSquareDeviations[$delimiter] < $min) { + $min = $meanSquareDeviations[$delimiter]; + $this->delimiter = $delimiter; + } + } + + return $this->delimiter; + } + + /** + * Get the next full line from the file. + * + * @return false|string + */ + public function getNextLine() + { + $line = ''; + $enclosure = ($this->escapeCharacter === '' ? '' + : ('(?escapeCharacter, '/') . ')')) + . preg_quote($this->enclosure, '/'); + + do { + // Get the next line in the file + $newLine = fgets($this->fileHandle); + + // Return false if there is no next line + if ($newLine === false) { + return false; + } + + // Add the new line to the line passed in + $line = $line . $newLine; + + // Drop everything that is enclosed to avoid counting false positives in enclosures + $line = (string) preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); + + // See if we have any enclosures left in the line + // if we still have an enclosure then we need to read the next line as well + } while (preg_match('/(' . $enclosure . ')/', $line) > 0); + + return ($line !== '') ? $line : false; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/DefaultReadFilter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/DefaultReadFilter.php new file mode 100644 index 0000000..0c4b87b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/DefaultReadFilter.php @@ -0,0 +1,18 @@ + + */ + private array $expressions = []; + + /** + * Spreadsheet shared across all functions. + */ + private Spreadsheet $spreadsheet; + + private ReferenceHelper $referenceHelper; + + /** @var array{'dataType': string[]} */ + public static array $mappings = [ + 'dataType' => [ + '10' => DataType::TYPE_NULL, + '20' => DataType::TYPE_BOOL, + '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel + '40' => DataType::TYPE_NUMERIC, // Float + '50' => DataType::TYPE_ERROR, + '60' => DataType::TYPE_STRING, + //'70': // Cell Range + //'80': // Array + ], + ]; + + /** + * Create a new Gnumeric. + */ + public function __construct() + { + parent::__construct(); + $this->referenceHelper = ReferenceHelper::getInstance(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + $data = null; + if (File::testFileNoThrow($filename)) { + $data = $this->gzfileGetContents($filename); + if (!str_contains($data, self::NAMESPACE_GNM)) { + $data = ''; + } + } + + return !empty($data); + } + + private static function matchXml(XMLReader $xml, string $expectedLocalName): bool + { + return $xml->namespaceURI === self::NAMESPACE_GNM + && $xml->localName === $expectedLocalName + && $xml->nodeType === XMLReader::ELEMENT; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. + * + * @return string[] + */ + public function listWorksheetNames(string $filename): array + { + File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an invalid Gnumeric file.'); + } + + $xml = new XMLReader(); + $contents = $this->gzfileGetContents($filename); + $xml->xml($contents); + $xml->setParserProperty(2, true); + + $worksheetNames = []; + while ($xml->read()) { + if (self::matchXml($xml, 'SheetName')) { + $xml->read(); // Move onto the value node + $worksheetNames[] = (string) $xml->value; + } elseif (self::matchXml($xml, 'Sheets')) { + // break out of the loop once we've got our sheet names rather than parse the entire file + break; + } + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an invalid Gnumeric file.'); + } + + $xml = new XMLReader(); + $contents = $this->gzfileGetContents($filename); + $xml->xml($contents); + $xml->setParserProperty(2, true); + + $worksheetInfo = []; + while ($xml->read()) { + if (self::matchXml($xml, 'Sheet')) { + $tmpInfo = [ + 'worksheetName' => '', + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ]; + $visibility = $xml->getAttribute('Visibility'); + if ((string) $visibility === self::GNM_SHEET_VISIBILITY_HIDDEN) { + $tmpInfo['sheetState'] = Worksheet::SHEETSTATE_HIDDEN; + } + + while ($xml->read()) { + if (self::matchXml($xml, 'Name')) { + $xml->read(); // Move onto the value node + $tmpInfo['worksheetName'] = (string) $xml->value; + } elseif (self::matchXml($xml, 'MaxCol')) { + $xml->read(); // Move onto the value node + $tmpInfo['lastColumnIndex'] = (int) $xml->value; + $tmpInfo['totalColumns'] = (int) $xml->value + 1; + } elseif (self::matchXml($xml, 'MaxRow')) { + $xml->read(); // Move onto the value node + $tmpInfo['totalRows'] = (int) $xml->value + 1; + + break; + } + } + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $worksheetInfo[] = $tmpInfo; + } + } + + return $worksheetInfo; + } + + private function gzfileGetContents(string $filename): string + { + $data = ''; + $contents = @file_get_contents($filename); + if ($contents !== false) { + if (str_starts_with($contents, "\x1f\x8b")) { + // Check if gzlib functions are available + if (function_exists('gzdecode')) { + $contents = @gzdecode($contents); + if ($contents !== false) { + $data = $contents; + } + } + } else { + $data = $contents; + } + } + if ($data !== '') { + $data = $this->getSecurityScannerOrThrow()->scan($data); + } + + return $data; + } + + /** @return mixed[] */ + public static function gnumericMappings(): array + { + return array_merge(self::$mappings, Styles::$mappings); + } + + private function processComments(SimpleXMLElement $sheet): void + { + if ((!$this->readDataOnly) && (isset($sheet->Objects))) { + foreach ($sheet->Objects->children(self::NAMESPACE_GNM) as $key => $comment) { + $commentAttributes = $comment->attributes(); + // Only comment objects are handled at the moment + if ($commentAttributes && $commentAttributes->Text) { + $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound) + ->setAuthor((string) $commentAttributes->Author) + ->setText($this->parseRichText((string) $commentAttributes->Text)); + } + } + } + } + + private static function testSimpleXml(mixed $value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(''); + } + + /** + * Loads Spreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + $spreadsheet->removeSheetByIndex(0); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Loads from file into Spreadsheet instance. + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + $this->spreadsheet = $spreadsheet; + File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an invalid Gnumeric file.'); + } + + $gFileData = $this->gzfileGetContents($filename); + + /** @var XmlScanner */ + $securityScanner = $this->securityScanner; + $xml2 = simplexml_load_string($securityScanner->scan($gFileData)); + $xml = self::testSimpleXml($xml2); + + $gnmXML = $xml->children(self::NAMESPACE_GNM); + (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML); + + $worksheetID = 0; + $sheetCreated = false; + foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) { + $sheet = self::testSimpleXml($sheetOrNull); + $worksheetName = (string) $sheet->Name; + if (is_array($this->loadSheetsOnly) && !in_array($worksheetName, $this->loadSheetsOnly, true)) { + continue; + } + + $maxRow = $maxCol = 0; + + // Create new Worksheet + $this->spreadsheet->createSheet(); + $sheetCreated = true; + $this->spreadsheet->setActiveSheetIndex($worksheetID); + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula + // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet + // name in line with the formula, not the reverse + $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + + $visibility = $sheet->attributes()['Visibility'] ?? self::GNM_SHEET_VISIBILITY_VISIBLE; + if ((string) $visibility !== self::GNM_SHEET_VISIBILITY_VISIBLE) { + $this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + } + + if (!$this->readDataOnly) { + (new PageSetup($this->spreadsheet)) + ->printInformation($sheet) + ->sheetMargins($sheet); + } + + foreach ($sheet->Cells->Cell as $cellOrNull) { + $cell = self::testSimpleXml($cellOrNull); + $cellAttributes = self::testSimpleXml($cell->attributes()); + $row = (int) $cellAttributes->Row + 1; + $column = (int) $cellAttributes->Col; + + $maxRow = max($maxRow, $row); + $maxCol = max($maxCol, $column); + + $column = Coordinate::stringFromColumnIndex($column + 1); + + // Read cell? + if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) { + continue; + } + + $this->loadCell($cell, $worksheetName, $cellAttributes, $column, $row); + } + + if ($sheet->Styles !== null) { + (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol); + } + + $this->processComments($sheet); + $this->processColumnWidths($sheet, $maxCol); + $this->processRowHeights($sheet, $maxRow); + $this->processMergedCells($sheet); + $this->processAutofilter($sheet); + + $this->setSelectedCells($sheet); + ++$worksheetID; + } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $this->spreadsheet->createSheet(); + } + + $this->processDefinedNames($gnmXML); + + $this->setSelectedSheet($gnmXML); + + // Return + return $this->spreadsheet; + } + + private function setSelectedSheet(SimpleXMLElement $gnmXML): void + { + if (isset($gnmXML->UIData)) { + $attributes = self::testSimpleXml($gnmXML->UIData->attributes()); + $selectedSheet = (int) $attributes['SelectedTab']; + $this->spreadsheet->setActiveSheetIndex($selectedSheet); + } + } + + private function setSelectedCells(?SimpleXMLElement $sheet): void + { + if ($sheet !== null && isset($sheet->Selections)) { + foreach ($sheet->Selections as $selection) { + $startCol = (int) ($selection->StartCol ?? 0); + $startRow = (int) ($selection->StartRow ?? 0) + 1; + $endCol = (int) ($selection->EndCol ?? $startCol); + $endRow = (int) ($selection->endRow ?? 0) + 1; + + $startColumn = Coordinate::stringFromColumnIndex($startCol + 1); + $endColumn = Coordinate::stringFromColumnIndex($endCol + 1); + + $startCell = "{$startColumn}{$startRow}"; + $endCell = "{$endColumn}{$endRow}"; + $selectedRange = $startCell . (($endCell !== $startCell) ? ':' . $endCell : ''); + $this->spreadsheet->getActiveSheet()->setSelectedCell($selectedRange); + + break; + } + } + } + + private function processMergedCells(?SimpleXMLElement $sheet): void + { + // Handle Merged Cells in this worksheet + if ($sheet !== null && isset($sheet->MergedRegions)) { + foreach ($sheet->MergedRegions->Merge as $mergeCells) { + if (str_contains((string) $mergeCells, ':')) { + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE); + } + } + } + } + + private function processAutofilter(?SimpleXMLElement $sheet): void + { + if ($sheet !== null && isset($sheet->Filters)) { + foreach ($sheet->Filters->Filter as $autofilter) { + $attributes = $autofilter->attributes(); + if (isset($attributes['Area'])) { + $this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']); + } + } + } + } + + private function setColumnWidth(int $whichColumn, float $defaultWidth): void + { + $this->spreadsheet->getActiveSheet() + ->getColumnDimension( + Coordinate::stringFromColumnIndex($whichColumn + 1) + ) + ->setWidth($defaultWidth); + } + + private function setColumnInvisible(int $whichColumn): void + { + $this->spreadsheet->getActiveSheet() + ->getColumnDimension( + Coordinate::stringFromColumnIndex($whichColumn + 1) + ) + ->setVisible(false); + } + + private function processColumnLoop(int $whichColumn, int $maxCol, ?SimpleXMLElement $columnOverride, float $defaultWidth): int + { + $columnOverride = self::testSimpleXml($columnOverride); + $columnAttributes = self::testSimpleXml($columnOverride->attributes()); + $column = $columnAttributes['No']; + $columnWidth = ((float) $columnAttributes['Unit']) / 5.4; + $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1'); + $columnCount = (int) ($columnAttributes['Count'] ?? 1); + while ($whichColumn < $column) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; + } + while (($whichColumn < ($column + $columnCount)) && ($whichColumn <= $maxCol)) { + $this->setColumnWidth($whichColumn, $columnWidth); + if ($hidden) { + $this->setColumnInvisible($whichColumn); + } + ++$whichColumn; + } + + return $whichColumn; + } + + private function processColumnWidths(?SimpleXMLElement $sheet, int $maxCol): void + { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Cols))) { + // Column Widths + $defaultWidth = 0; + $columnAttributes = $sheet->Cols->attributes(); + if ($columnAttributes !== null) { + $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4; + } + $whichColumn = 0; + foreach ($sheet->Cols->ColInfo as $columnOverride) { + $whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth); + } + while ($whichColumn <= $maxCol) { + $this->setColumnWidth($whichColumn, $defaultWidth); + ++$whichColumn; + } + } + } + + private function setRowHeight(int $whichRow, float $defaultHeight): void + { + $this->spreadsheet + ->getActiveSheet() + ->getRowDimension($whichRow) + ->setRowHeight($defaultHeight); + } + + private function setRowInvisible(int $whichRow): void + { + $this->spreadsheet + ->getActiveSheet() + ->getRowDimension($whichRow) + ->setVisible(false); + } + + private function processRowLoop(int $whichRow, int $maxRow, ?SimpleXMLElement $rowOverride, float $defaultHeight): int + { + $rowOverride = self::testSimpleXml($rowOverride); + $rowAttributes = self::testSimpleXml($rowOverride->attributes()); + $row = $rowAttributes['No']; + $rowHeight = (float) $rowAttributes['Unit']; + $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); + $rowCount = (int) ($rowAttributes['Count'] ?? 1); + while ($whichRow < $row) { + ++$whichRow; + $this->setRowHeight($whichRow, $defaultHeight); + } + while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) { + ++$whichRow; + $this->setRowHeight($whichRow, $rowHeight); + if ($hidden) { + $this->setRowInvisible($whichRow); + } + } + + return $whichRow; + } + + private function processRowHeights(?SimpleXMLElement $sheet, int $maxRow): void + { + if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Rows))) { + // Row Heights + $defaultHeight = 0; + $rowAttributes = $sheet->Rows->attributes(); + if ($rowAttributes !== null) { + $defaultHeight = (float) $rowAttributes['DefaultSizePts']; + } + $whichRow = 0; + + foreach ($sheet->Rows->RowInfo as $rowOverride) { + $whichRow = $this->processRowLoop($whichRow, $maxRow, $rowOverride, $defaultHeight); + } + // never executed, I can't figure out any circumstances + // under which it would be executed, and, even if + // such exist, I'm not convinced this is needed. + //while ($whichRow < $maxRow) { + // ++$whichRow; + // $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow)->setRowHeight($defaultHeight); + //} + } + } + + private function processDefinedNames(?SimpleXMLElement $gnmXML): void + { + // Loop through definedNames (global named ranges) + if ($gnmXML !== null && isset($gnmXML->Names)) { + foreach ($gnmXML->Names->Name as $definedName) { + $name = (string) $definedName->name; + $value = (string) $definedName->value; + if (stripos($value, '#REF!') !== false || empty($value)) { + continue; + } + + $value = str_replace("\\'", "''", $value); + [$worksheetName] = Worksheet::extractSheetTitle($value, true, true); + $worksheet = $this->spreadsheet->getSheetByName($worksheetName); + // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet + if ($worksheet !== null) { + $this->spreadsheet->addDefinedName(DefinedName::createInstance($name, $worksheet, $value)); + } + } + } + } + + private function parseRichText(string $is): RichText + { + $value = new RichText(); + $value->createText($is); + + return $value; + } + + private function loadCell( + SimpleXMLElement $cell, + string $worksheetName, + SimpleXMLElement $cellAttributes, + string $column, + int $row + ): void { + $ValueType = $cellAttributes->ValueType; + $ExprID = (string) $cellAttributes->ExprID; + $rows = (int) ($cellAttributes->Rows ?? 0); + $cols = (int) ($cellAttributes->Cols ?? 0); + $type = DataType::TYPE_FORMULA; + $isArrayFormula = ($rows > 0 && $cols > 0); + $arrayFormulaRange = $isArrayFormula ? $this->getArrayFormulaRange($column, $row, $cols, $rows) : null; + if ($ExprID > '') { + if (((string) $cell) > '') { + // Formula + $this->expressions[$ExprID] = [ + 'column' => (int) $cellAttributes->Col, + 'row' => (int) $cellAttributes->Row, + 'formula' => (string) $cell, + ]; + } else { + // Shared Formula + $expression = $this->expressions[$ExprID]; + + $cell = $this->referenceHelper->updateFormulaReferences( + $expression['formula'], + 'A1', + $cellAttributes->Col - $expression['column'], + $cellAttributes->Row - $expression['row'], + $worksheetName + ); + } + $type = DataType::TYPE_FORMULA; + } elseif ($isArrayFormula === false) { + $vtype = (string) $ValueType; + if (array_key_exists($vtype, self::$mappings['dataType'])) { + $type = self::$mappings['dataType'][$vtype]; + } + if ($vtype === '20') { // Boolean + $cell = $cell == 'TRUE'; + } + } + + $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); + if ($arrayFormulaRange === null) { + $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(null); + } else { + $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayFormulaRange]); + } + if (isset($cellAttributes->ValueFormat)) { + $this->spreadsheet->getActiveSheet()->getCell($column . $row) + ->getStyle()->getNumberFormat() + ->setFormatCode((string) $cellAttributes->ValueFormat); + } + } + + private function getArrayFormulaRange(string $column, int $row, int $cols, int $rows): string + { + $arrayFormulaRange = $column . $row; + $arrayFormulaRange .= ':' + . Coordinate::stringFromColumnIndex( + Coordinate::columnIndexFromString($column) + + $cols - 1 + ) + . (string) ($row + $rows - 1); + + return $arrayFormulaRange; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php new file mode 100644 index 0000000..8315aa5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php @@ -0,0 +1,153 @@ +spreadsheet = $spreadsheet; + } + + public function printInformation(SimpleXMLElement $sheet): self + { + if (isset($sheet->PrintInformation, $sheet->PrintInformation[0])) { + $printInformation = $sheet->PrintInformation[0]; + $setup = $this->spreadsheet->getActiveSheet()->getPageSetup(); + + $attributes = $printInformation->Scale->attributes(); + if (isset($attributes['percentage'])) { + $setup->setScale((int) $attributes['percentage']); + } + $pageOrder = (string) $printInformation->order; + if ($pageOrder === 'r_then_d') { + $setup->setPageOrder(WorksheetPageSetup::PAGEORDER_OVER_THEN_DOWN); + } elseif ($pageOrder === 'd_then_r') { + $setup->setPageOrder(WorksheetPageSetup::PAGEORDER_DOWN_THEN_OVER); + } + $orientation = (string) $printInformation->orientation; + if ($orientation !== '') { + $setup->setOrientation($orientation); + } + $attributes = $printInformation->hcenter->attributes(); + if (isset($attributes['value'])) { + $setup->setHorizontalCentered((bool) (string) $attributes['value']); + } + $attributes = $printInformation->vcenter->attributes(); + if (isset($attributes['value'])) { + $setup->setVerticalCentered((bool) (string) $attributes['value']); + } + } + + return $this; + } + + public function sheetMargins(SimpleXMLElement $sheet): self + { + if (isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) { + $marginSet = [ + // Default Settings + 'top' => 0.75, + 'header' => 0.3, + 'left' => 0.7, + 'right' => 0.7, + 'bottom' => 0.75, + 'footer' => 0.3, + ]; + + $marginSet = $this->buildMarginSet($sheet, $marginSet); + $this->adjustMargins($marginSet); + } + + return $this; + } + + /** + * @param float[] $marginSet + * + * @return float[] + */ + private function buildMarginSet(SimpleXMLElement $sheet, array $marginSet): array + { + foreach ($sheet->PrintInformation->Margins->children(Gnumeric::NAMESPACE_GNM) as $key => $margin) { + $marginAttributes = $margin->attributes(); + $marginSize = ($marginAttributes['Points']) ?? 72; // Default is 72pt + // Convert value in points to inches + $marginSize = PageMargins::fromPoints((float) $marginSize); + $marginSet[$key] = $marginSize; + } + + return $marginSet; + } + + /** @param float[] $marginSet */ + private function adjustMargins(array $marginSet): void + { + foreach ($marginSet as $key => $marginSize) { + // Gnumeric is quirky in the way it displays the header/footer values: + // header is actually the sum of top and header; footer is actually the sum of bottom and footer + // then top is actually the header value, and bottom is actually the footer value + switch ($key) { + case 'left': + case 'right': + $this->sheetMargin($key, $marginSize); + + break; + case 'top': + $this->sheetMargin($key, $marginSet['header'] ?? 0); + + break; + case 'bottom': + $this->sheetMargin($key, $marginSet['footer'] ?? 0); + + break; + case 'header': + $this->sheetMargin($key, ($marginSet['top'] ?? 0) - $marginSize); + + break; + case 'footer': + $this->sheetMargin($key, ($marginSet['bottom'] ?? 0) - $marginSize); + + break; + } + } + } + + private function sheetMargin(string $key, float $marginSize): void + { + switch ($key) { + case 'top': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setTop($marginSize); + + break; + case 'bottom': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom($marginSize); + + break; + case 'left': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft($marginSize); + + break; + case 'right': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setRight($marginSize); + + break; + case 'header': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader($marginSize); + + break; + case 'footer': + $this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter($marginSize); + + break; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php new file mode 100644 index 0000000..0ff6647 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/Properties.php @@ -0,0 +1,159 @@ +spreadsheet = $spreadsheet; + } + + private function docPropertiesOld(SimpleXMLElement $gnmXML): void + { + $docProps = $this->spreadsheet->getProperties(); + foreach ($gnmXML->Summary->Item as $summaryItem) { + $propertyName = $summaryItem->name; + $propertyValue = $summaryItem->{'val-string'}; + switch ($propertyName) { + case 'title': + $docProps->setTitle(trim($propertyValue)); + + break; + case 'comments': + $docProps->setDescription(trim($propertyValue)); + + break; + case 'keywords': + $docProps->setKeywords(trim($propertyValue)); + + break; + case 'category': + $docProps->setCategory(trim($propertyValue)); + + break; + case 'manager': + $docProps->setManager(trim($propertyValue)); + + break; + case 'author': + $docProps->setCreator(trim($propertyValue)); + $docProps->setLastModifiedBy(trim($propertyValue)); + + break; + case 'company': + $docProps->setCompany(trim($propertyValue)); + + break; + } + } + } + + private function docPropertiesDC(SimpleXMLElement $officePropertyDC): void + { + $docProps = $this->spreadsheet->getProperties(); + foreach ($officePropertyDC as $propertyName => $propertyValue) { + $propertyValue = trim((string) $propertyValue); + switch ($propertyName) { + case 'title': + $docProps->setTitle($propertyValue); + + break; + case 'subject': + $docProps->setSubject($propertyValue); + + break; + case 'creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); + + break; + case 'date': + $creationDate = $propertyValue; + $docProps->setModified($creationDate); + + break; + case 'description': + $docProps->setDescription($propertyValue); + + break; + } + } + } + + private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta): void + { + $docProps = $this->spreadsheet->getProperties(); + foreach ($officePropertyMeta as $propertyName => $propertyValue) { + $attributes = $propertyValue->attributes(Gnumeric::NAMESPACE_META); + $propertyValue = trim((string) $propertyValue); + switch ($propertyName) { + case 'keyword': + $docProps->setKeywords($propertyValue); + + break; + case 'initial-creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); + + break; + case 'creation-date': + $creationDate = $propertyValue; + $docProps->setCreated($creationDate); + + break; + case 'user-defined': + if ($attributes) { + [, $attrName] = explode(':', (string) $attributes['name']); + $this->userDefinedProperties($attrName, $propertyValue); + } + + break; + } + } + } + + private function userDefinedProperties(string $attrName, string $propertyValue): void + { + $docProps = $this->spreadsheet->getProperties(); + switch ($attrName) { + case 'publisher': + $docProps->setCompany($propertyValue); + + break; + case 'category': + $docProps->setCategory($propertyValue); + + break; + case 'manager': + $docProps->setManager($propertyValue); + + break; + } + } + + public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML): void + { + $officeXML = $xml->children(Gnumeric::NAMESPACE_OFFICE); + if (!empty($officeXML)) { + $officeDocXML = $officeXML->{'document-meta'}; + $officeDocMetaXML = $officeDocXML->meta; + + foreach ($officeDocMetaXML as $officePropertyData) { + $officePropertyDC = $officePropertyData->children(Gnumeric::NAMESPACE_DC); + $this->docPropertiesDC($officePropertyDC); + + $officePropertyMeta = $officePropertyData->children(Gnumeric::NAMESPACE_META); + $this->docPropertiesMeta($officePropertyMeta); + } + } elseif (isset($gnmXML->Summary)) { + $this->docPropertiesOld($gnmXML); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php new file mode 100644 index 0000000..0729f2e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -0,0 +1,295 @@ + */ + public static array $mappings = [ + 'borderStyle' => [ + '0' => Border::BORDER_NONE, + '1' => Border::BORDER_THIN, + '2' => Border::BORDER_MEDIUM, + '3' => Border::BORDER_SLANTDASHDOT, + '4' => Border::BORDER_DASHED, + '5' => Border::BORDER_THICK, + '6' => Border::BORDER_DOUBLE, + '7' => Border::BORDER_DOTTED, + '8' => Border::BORDER_MEDIUMDASHED, + '9' => Border::BORDER_DASHDOT, + '10' => Border::BORDER_MEDIUMDASHDOT, + '11' => Border::BORDER_DASHDOTDOT, + '12' => Border::BORDER_MEDIUMDASHDOTDOT, + '13' => Border::BORDER_MEDIUMDASHDOTDOT, + ], + 'fillType' => [ + '1' => Fill::FILL_SOLID, + '2' => Fill::FILL_PATTERN_DARKGRAY, + '3' => Fill::FILL_PATTERN_MEDIUMGRAY, + '4' => Fill::FILL_PATTERN_LIGHTGRAY, + '5' => Fill::FILL_PATTERN_GRAY125, + '6' => Fill::FILL_PATTERN_GRAY0625, + '7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe + '8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe + '9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe + '10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe + '11' => Fill::FILL_PATTERN_DARKGRID, // diagonal crosshatch + '12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch + '13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + '14' => Fill::FILL_PATTERN_LIGHTVERTICAL, + '15' => Fill::FILL_PATTERN_LIGHTUP, + '16' => Fill::FILL_PATTERN_LIGHTDOWN, + '17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch + '18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch + ], + 'horizontal' => [ + '1' => Alignment::HORIZONTAL_GENERAL, + '2' => Alignment::HORIZONTAL_LEFT, + '4' => Alignment::HORIZONTAL_RIGHT, + '8' => Alignment::HORIZONTAL_CENTER, + '16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + '32' => Alignment::HORIZONTAL_JUSTIFY, + '64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + ], + 'underline' => [ + '1' => Font::UNDERLINE_SINGLE, + '2' => Font::UNDERLINE_DOUBLE, + '3' => Font::UNDERLINE_SINGLEACCOUNTING, + '4' => Font::UNDERLINE_DOUBLEACCOUNTING, + ], + 'vertical' => [ + '1' => Alignment::VERTICAL_TOP, + '2' => Alignment::VERTICAL_BOTTOM, + '4' => Alignment::VERTICAL_CENTER, + '8' => Alignment::VERTICAL_JUSTIFY, + ], + ]; + + public function __construct(Spreadsheet $spreadsheet, bool $readDataOnly) + { + $this->spreadsheet = $spreadsheet; + $this->readDataOnly = $readDataOnly; + } + + public function read(SimpleXMLElement $sheet, int $maxRow, int $maxCol): void + { + if ($sheet->Styles->StyleRegion !== null) { + $this->readStyles($sheet->Styles->StyleRegion, $maxRow, $maxCol); + } + } + + private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $maxCol): void + { + foreach ($styleRegion as $style) { + $styleAttributes = $style->attributes(); + if ($styleAttributes !== null && ($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { + $cellRange = $this->readStyleRange($styleAttributes, $maxCol, $maxRow); + + $styleAttributes = $style->Style->attributes(); + + /** @var mixed[][] */ + $styleArray = []; + // We still set the number format mask for date/time values, even if readDataOnly is true + // so that we can identify whether a float is a float or a date value + $formatCode = $styleAttributes ? (string) $styleAttributes['Format'] : null; + if ($formatCode && Date::isDateTimeFormatCode($formatCode)) { + $styleArray['numberFormat']['formatCode'] = $formatCode; + } + if ($this->readDataOnly === false && $styleAttributes !== null) { + // If readDataOnly is false, we set all formatting information + $styleArray['numberFormat']['formatCode'] = $formatCode; + $styleArray = $this->readStyle($styleArray, $styleAttributes, $style); + } + /** @var mixed[][] $styleArray */ + $this->spreadsheet + ->getActiveSheet() + ->getStyle($cellRange) + ->applyFromArray($styleArray); + } + } + } + + /** @param mixed[][] $styleArray */ + private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void + { + if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; + } elseif (isset($srssb->Diagonal)) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; + } elseif (isset($srssb->{'Rev-Diagonal'})) { + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); + $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; + } + } + + /** @param mixed[][] $styleArray */ + private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void + { + $ucDirection = ucfirst($direction); + if (isset($srssb->$ucDirection)) { + /** @var SimpleXMLElement */ + $temp = $srssb->$ucDirection; + $styleArray['borders'][$direction] = self::parseBorderAttributes($temp->attributes()); + } + } + + private function calcRotation(SimpleXMLElement $styleAttributes): int + { + $rotation = (int) $styleAttributes->Rotation; + if ($rotation >= 270 && $rotation <= 360) { + $rotation -= 360; + } + $rotation = (abs($rotation) > 90) ? 0 : $rotation; + + return $rotation; + } + + /** @param mixed[][] $styleArray */ + private static function addStyle(array &$styleArray, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key] = self::$mappings[$key][$value]; //* @phpstan-ignore-line + } + } + + /** @param mixed[][] $styleArray */ + private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void + { + if (array_key_exists($value, self::$mappings[$key])) { + $styleArray[$key1][$key] = self::$mappings[$key][$value]; + } + } + + /** @return mixed[][] */ + private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array + { + /** @var mixed[][] */ + $styleArray = []; + if ($borderAttributes !== null) { + if (isset($borderAttributes['Color'])) { + $styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); + } + + self::addStyle($styleArray, 'borderStyle', (string) $borderAttributes['Style']); + } + /** @var mixed[][] $styleArray */ + + return $styleArray; + } + + private static function parseGnumericColour(string $gnmColour): string + { + [$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); + $gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); + $gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2); + $gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2); + + return $gnmR . $gnmG . $gnmB; + } + + /** @param mixed[][] $styleArray */ + private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void + { + $RGB = self::parseGnumericColour((string) $styleAttributes['Fore']); + /** @var mixed[][][] $styleArray */ + $styleArray['font']['color']['rgb'] = $RGB; + $RGB = self::parseGnumericColour((string) $styleAttributes['Back']); + $shade = (string) $styleAttributes['Shade']; + if (($RGB !== '000000') || ($shade !== '0')) { + $RGB2 = self::parseGnumericColour((string) $styleAttributes['PatternColor']); + if ($shade === '1') { + $styleArray['fill']['startColor']['rgb'] = $RGB; + $styleArray['fill']['endColor']['rgb'] = $RGB2; + } else { + $styleArray['fill']['endColor']['rgb'] = $RGB; + $styleArray['fill']['startColor']['rgb'] = $RGB2; + } + self::addStyle2($styleArray, 'fill', 'fillType', $shade); + } + } + + private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string + { + $startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); + $startRow = $styleAttributes['startRow'] + 1; + + $endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; + $endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); + + $endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); + $cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; + + return $cellRange; + } + + /** + * @param mixed[][] $styleArray + * + * @return mixed[] + */ + private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array + { + self::addStyle2($styleArray, 'alignment', 'horizontal', (string) $styleAttributes['HAlign']); + self::addStyle2($styleArray, 'alignment', 'vertical', (string) $styleAttributes['VAlign']); + $styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; + $styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); + $styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; + $styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; + + $this->addColors($styleArray, $styleAttributes); + + $fontAttributes = $style->Style->Font->attributes(); + if ($fontAttributes !== null) { + $styleArray['font']['name'] = (string) $style->Style->Font; + $styleArray['font']['size'] = (int) ($fontAttributes['Unit']); + $styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; + $styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; + $styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; + self::addStyle2($styleArray, 'font', 'underline', (string) $fontAttributes['Underline']); + + switch ($fontAttributes['Script']) { + case '1': + $styleArray['font']['superscript'] = true; + + break; + case '-1': + $styleArray['font']['subscript'] = true; + + break; + } + } + + if (isset($style->Style->StyleBorder)) { + $srssb = $style->Style->StyleBorder; + $this->addBorderStyle($srssb, $styleArray, 'top'); + $this->addBorderStyle($srssb, $styleArray, 'bottom'); + $this->addBorderStyle($srssb, $styleArray, 'left'); + $this->addBorderStyle($srssb, $styleArray, 'right'); + $this->addBorderDiagonal($srssb, $styleArray); + } + // TO DO + /* + if (isset($style->Style->HyperLink)) { + $hyperlink = $style->Style->HyperLink->attributes(); + } + */ + + return $styleArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Html.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Html.php new file mode 100644 index 0000000..49b5202 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Html.php @@ -0,0 +1,1303 @@ + [ + 'font' => [ + 'bold' => true, + 'size' => 24, + ], + ], // Bold, 24pt + 'h2' => [ + 'font' => [ + 'bold' => true, + 'size' => 18, + ], + ], // Bold, 18pt + 'h3' => [ + 'font' => [ + 'bold' => true, + 'size' => 13.5, + ], + ], // Bold, 13.5pt + 'h4' => [ + 'font' => [ + 'bold' => true, + 'size' => 12, + ], + ], // Bold, 12pt + 'h5' => [ + 'font' => [ + 'bold' => true, + 'size' => 10, + ], + ], // Bold, 10pt + 'h6' => [ + 'font' => [ + 'bold' => true, + 'size' => 7.5, + ], + ], // Bold, 7.5pt + 'a' => [ + 'font' => [ + 'underline' => true, + 'color' => [ + 'argb' => Color::COLOR_BLUE, + ], + ], + ], // Blue underlined + 'hr' => [ + 'borders' => [ + 'bottom' => [ + 'borderStyle' => Border::BORDER_THIN, + 'color' => [ + Color::COLOR_BLACK, + ], + ], + ], + ], // Bottom border + 'strong' => [ + 'font' => [ + 'bold' => true, + ], + ], // Bold + 'b' => [ + 'font' => [ + 'bold' => true, + ], + ], // Bold + 'i' => [ + 'font' => [ + 'italic' => true, + ], + ], // Italic + 'em' => [ + 'font' => [ + 'italic' => true, + ], + ], // Italic + ]; + + /** @var array */ + protected array $rowspan = []; + + /** + * Create a new HTML Reader instance. + */ + public function __construct() + { + parent::__construct(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Validate that the current file is an HTML file. + */ + public function canRead(string $filename): bool + { + // Check if file exists + try { + $this->openFile($filename); + } catch (Exception) { + return false; + } + + $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? ''; + + $startWithTag = self::startsWithTag($beginning); + $containsTags = self::containsTags($beginning); + $endsWithTag = self::endsWithTag($this->readEnding()); + + fclose($this->fileHandle); + + return $startWithTag && $containsTags && $endsWithTag; + } + + private function readBeginning(): string + { + fseek($this->fileHandle, 0); + + return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE); + } + + private function readEnding(): string + { + $meta = stream_get_meta_data($this->fileHandle); + // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3 + $filename = $meta['uri']; //@phpstan-ignore-line + + clearstatcache(true, $filename); + $size = (int) filesize($filename); + if ($size === 0) { + return ''; + } + + $blockSize = self::TEST_SAMPLE_SIZE; + if ($size < $blockSize) { + $blockSize = $size; + } + + fseek($this->fileHandle, $size - $blockSize); + + return (string) fread($this->fileHandle, $blockSize); + } + + private static function startsWithTag(string $data): bool + { + return str_starts_with(trim($data), '<'); + } + + private static function endsWithTag(string $data): bool + { + return str_ends_with(trim($data), '>'); + } + + private static function containsTags(string $data): bool + { + return strlen($data) !== strlen(strip_tags($data)); + } + + /** + * Loads Spreadsheet from file. + */ + public function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Data Array used for testing only, should write to + * Spreadsheet object on completion of tests. + * + * @var mixed[][] + */ + protected array $dataArray = []; + + protected int $tableLevel = 0; + + /** @var string[] */ + protected array $nestedColumn = ['A']; + + protected function setTableStartColumn(string $column): string + { + if ($this->tableLevel == 0) { + $column = 'A'; + } + ++$this->tableLevel; + $this->nestedColumn[$this->tableLevel] = $column; + + return $this->nestedColumn[$this->tableLevel]; + } + + protected function getTableStartColumn(): string + { + return $this->nestedColumn[$this->tableLevel]; + } + + protected function releaseTableStartColumn(): string + { + --$this->tableLevel; + + return array_pop($this->nestedColumn) ?? ''; + } + + /** + * Flush cell. + * + * @param string[] $attributeArray + * + * @param-out string $cellContent In one case, it can be bool + */ + protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void + { + if (is_string($cellContent)) { + // Simple String content + if (trim($cellContent) > '') { + // Only actually write it if there's content in the string + // Write to worksheet to be done here... + // ... we return the cell, so we can mess about with styles more easily + + // Set cell value explicitly if there is data-type attribute + if (isset($attributeArray['data-type'])) { + $datatype = $attributeArray['data-type']; + if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) { + //Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number + if (str_starts_with($cellContent, '=')) { + $sheet->getCell($column . $row) + ->getStyle() + ->setQuotePrefix(true); + } + } + if ($datatype === DataType::TYPE_BOOL) { + // This is the case where we can set cellContent to bool rather than string + $cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line + if (!is_bool($cellContent)) { + $attributeArray['data-type'] = DataType::TYPE_STRING; + } + } + + //catching the Exception and ignoring the invalid data types + try { + $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']); + } catch (SpreadsheetException) { + $sheet->setCellValue($column . $row, $cellContent); + } + } else { + $sheet->setCellValue($column . $row, $cellContent); + } + $this->dataArray[$row][$column] = $cellContent; + } + } else { + // We have a Rich Text run + // TODO + $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent); + } + $cellContent = (string) ''; + } + + /** @var array> */ + private static array $falseTrueArray = []; + + private static function convertBoolean(?string $cellContent): bool|string + { + if ($cellContent === '1') { + return true; + } + if ($cellContent === '0' || $cellContent === '' || $cellContent === null) { + return false; + } + if (empty(self::$falseTrueArray)) { + $calc = Calculation::getInstance(); + self::$falseTrueArray = $calc->getFalseTrueArray(); + } + if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) { + return true; + } + if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) { + return false; + } + + return $cellContent; + } + + private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void + { + $attributeArray = []; + /** @var DOMAttr $attribute */ + foreach (($child->attributes ?? []) as $attribute) { + $attributeArray[$attribute->name] = $attribute->value; + } + + if ($child->nodeName === 'body') { + $row = 1; + $column = 'A'; + $cellContent = ''; + $this->tableLevel = 0; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'title') { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + try { + $sheet->setTitle($cellContent, true, true); + $sheet->getParent()?->getProperties()?->setTitle($cellContent); + } catch (SpreadsheetException) { + // leave default title if too long or illegal chars + } + $cellContent = ''; + } else { + $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b']; + + /** @param string[] $attributeArray */ + private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) { + if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') { + $sheet->getComment($column . $row) + ->getText() + ->createTextRun($child->textContent); + if (isset($attributeArray['dir']) && $attributeArray['dir'] === 'rtl') { + $sheet->getComment($column . $row)->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL); + } + if (isset($attributeArray['style'])) { + $alignStyle = $attributeArray['style']; + if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches) === 1) { + $sheet->getComment($column . $row)->setAlignment($matches[1]); + } + } + } else { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } + + if (isset(self::FORMATS[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]); + } + } else { + $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'hr') { + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + ++$row; + $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]); + ++$row; + } + // fall through to br + $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + + /** @param string[] $attributeArray */ + private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'br' || $child->nodeName === 'hr') { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a newline and set the cell to wrap + $cellContent .= "\n"; + $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); + } else { + // Otherwise flush our existing content and move the row cursor on + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + ++$row; + } + } else { + $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'a') { + foreach ($attributeArray as $attributeName => $attributeValue) { + switch ($attributeName) { + case 'href': + $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue); + $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]); + + break; + case 'class': + if ($attributeValue === 'comment-indicator') { + break; // Ignore - it's just a red square. + } + } + } + // no idea why this should be needed + //$cellContent .= ' '; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p']; + + /** @param string[] $attributeArray */ + private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if (in_array((string) $child->nodeName, self::H1_ETC, true)) { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a newline + $cellContent .= $cellContent ? "\n" : ''; + $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true); + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + if ($cellContent > '') { + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + ++$row; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + + if (isset(self::FORMATS[$child->nodeName])) { + $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]); + } + + ++$row; + $column = 'A'; + } + } else { + $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'li') { + if ($this->tableLevel > 0) { + // If we're inside a table, replace with a newline + $cellContent .= $cellContent ? "\n" : ''; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + if ($cellContent > '') { + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + } + ++$row; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + $column = 'A'; + } + } else { + $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'img') { + $this->insertImage($sheet, $column, $row, $attributeArray); + } else { + $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + private string $currentColumn = 'A'; + + /** @param string[] $attributeArray */ + private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'table') { + if (isset($attributeArray['class'])) { + $classes = explode(' ', $attributeArray['class']); + $sheet->setShowGridlines(in_array('gridlines', $classes, true)); + $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true)); + } + if (isset($attributeArray['data-printarea'])) { + $sheet->getPageSetup() + ->setPrintArea($attributeArray['data-printarea']); + } + if ('rtl' === ($attributeArray['dir'] ?? '')) { + $sheet->setRightToLeft(true); + } + $this->currentColumn = 'A'; + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + $column = $this->setTableStartColumn($column); + if ($this->tableLevel > 1 && $row > 1) { + --$row; + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + $column = $this->releaseTableStartColumn(); + if ($this->tableLevel > 1) { + StringHelper::stringIncrement($column); + } else { + ++$row; + } + } else { + $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName === 'col') { + $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray); + StringHelper::stringIncrement($this->currentColumn); + } elseif ($child->nodeName === 'tr') { + $column = $this->getTableStartColumn(); + $cellContent = ''; + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + if (isset($attributeArray['height'])) { + $sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']); + } + + ++$row; + } else { + $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + if ($child->nodeName !== 'td' && $child->nodeName !== 'th') { + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + } else { + $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['bgcolor'])) { + $sheet->getStyle("$column$row")->applyFromArray( + [ + 'fill' => [ + 'fillType' => Fill::FILL_SOLID, + 'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])], + ], + ] + ); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void + { + if (isset($attributeArray['width'])) { + $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width()); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void + { + if (isset($attributeArray['height'])) { + $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height()); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['align'])) { + $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['valign'])) { + $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void + { + if (isset($attributeArray['data-format'])) { + $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']); + } + } + + /** @param string[] $attributeArray */ + private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void + { + while (isset($this->rowspan[$column . $row])) { + $temp = (string) $column; + $column = StringHelper::stringIncrement($temp); + } + $this->processDomElement($child, $sheet, $row, $column, $cellContent); + + // apply inline style + $this->applyInlineStyle($sheet, $row, $column, $attributeArray); + + /** @var string $cellContent */ + $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); + + $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray); + $this->processDomElementWidth($sheet, $column, $attributeArray); + $this->processDomElementHeight($sheet, $row, $attributeArray); + $this->processDomElementAlign($sheet, $row, $column, $attributeArray); + $this->processDomElementVAlign($sheet, $row, $column, $attributeArray); + $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray); + + if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + //create merging rowspan and colspan + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + StringHelper::stringIncrement($columnTo); + } + $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); + foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { + $this->rowspan[$value] = true; + } + $sheet->mergeCells($range); + $column = $columnTo; + } elseif (isset($attributeArray['rowspan'])) { + //create merging rowspan + $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); + foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { + $this->rowspan[$value] = true; + } + $sheet->mergeCells($range); + } elseif (isset($attributeArray['colspan'])) { + //create merging colspan + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + StringHelper::stringIncrement($columnTo); + } + $sheet->mergeCells($column . $row . ':' . $columnTo . $row); + $column = $columnTo; + } + + StringHelper::stringIncrement($column); + } + + protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void + { + foreach ($element->childNodes as $child) { + if ($child instanceof DOMText) { + $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? '')); + if ($domText === "\u{a0}") { + $domText = ''; + } + // simply append the text if the cell content is a plain text string + $cellContent .= $domText; + // but if we have a rich text run instead, we need to append it correctly + // TODO + } elseif ($child instanceof DOMElement) { + $this->processDomElementBody($sheet, $row, $column, $cellContent, $child); + } + } + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + // Validate + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an Invalid HTML file.'); + } + + // Create a new DOM object + $dom = new DOMDocument(); + + // Reload the HTML file into the DOM object + try { + $convert = $this->getSecurityScannerOrThrow()->scanFile($filename); + $convert = static::replaceNonAsciiIfNeeded($convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); + } catch (Throwable $e) { + $loaded = false; + } + if ($loaded === false) { + throw new Exception('Failed to load file ' . $filename . ' as a DOM Document', 0, $e ?? null); + } + self::loadProperties($dom, $spreadsheet); + + return $this->loadDocument($dom, $spreadsheet); + } + + private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void + { + $properties = $spreadsheet->getProperties(); + foreach ($dom->getElementsByTagName('meta') as $meta) { + $metaContent = (string) $meta->getAttribute('content'); + if ($metaContent !== '') { + $metaName = (string) $meta->getAttribute('name'); + switch ($metaName) { + case 'author': + $properties->setCreator($metaContent); + + break; + case 'category': + $properties->setCategory($metaContent); + + break; + case 'company': + $properties->setCompany($metaContent); + + break; + case 'created': + $properties->setCreated($metaContent); + + break; + case 'description': + $properties->setDescription($metaContent); + + break; + case 'keywords': + $properties->setKeywords($metaContent); + + break; + case 'lastModifiedBy': + $properties->setLastModifiedBy($metaContent); + + break; + case 'manager': + $properties->setManager($metaContent); + + break; + case 'modified': + $properties->setModified($metaContent); + + break; + case 'subject': + $properties->setSubject($metaContent); + + break; + case 'title': + $properties->setTitle($metaContent); + + break; + case 'viewport': + $properties->setViewport($metaContent); + + break; + default: + if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) { + match ($matches[1]) { + 'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN), + 'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT), + 'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER), + 'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE), + // string + default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING), + }; + } + } + } + } + if (!empty($dom->baseURI)) { + $properties->setHyperlinkBase($dom->baseURI); + } + } + + /** @param string[] $matches */ + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + + protected static function replaceNonAsciiIfNeeded(string $convert): ?string + { + if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) { + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable $callback */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + } + + return $convert; + } + + /** + * Spreadsheet from content. + */ + public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet + { + // Create a new DOM object + $dom = new DOMDocument(); + + // Reload the HTML file into the DOM object + try { + $convert = $this->getSecurityScannerOrThrow()->scan($content); + $convert = static::replaceNonAsciiIfNeeded($convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); + } catch (Throwable $e) { + $loaded = false; + } + if ($loaded === false) { + throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null); + } + $spreadsheet = $spreadsheet ?? $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + self::loadProperties($dom, $spreadsheet); + + return $this->loadDocument($dom, $spreadsheet); + } + + /** + * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance. + */ + private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet + { + while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { + $spreadsheet->createSheet(); + } + $spreadsheet->setActiveSheetIndex($this->sheetIndex); + + // Discard white space + $document->preserveWhiteSpace = false; + + $row = 0; + $column = 'A'; + $content = ''; + $this->rowspan = []; + $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content); + + // Return + return $spreadsheet; + } + + /** + * Get sheet index. + */ + public function getSheetIndex(): int + { + return $this->sheetIndex; + } + + /** + * Set sheet index. + * + * @param int $sheetIndex Sheet index + * + * @return $this + */ + public function setSheetIndex(int $sheetIndex): static + { + $this->sheetIndex = $sheetIndex; + + return $this; + } + + /** + * Apply inline css inline style. + * + * NOTES : + * Currently only intended for td & th element, + * and only takes 'background-color' and 'color'; property with HEX color + * + * TODO : + * - Implement to other properties, such as border + * + * @param string[] $attributeArray + */ + private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void + { + if (!isset($attributeArray['style'])) { + return; + } + + if ($row <= 0 || $column === '') { + $cellStyle = new Style(); + } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + StringHelper::stringIncrement($columnTo); + } + $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); + $cellStyle = $sheet->getStyle($range); + } elseif (isset($attributeArray['rowspan'])) { + $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); + $cellStyle = $sheet->getStyle($range); + } elseif (isset($attributeArray['colspan'])) { + $columnTo = $column; + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { + StringHelper::stringIncrement($columnTo); + } + $range = $column . $row . ':' . $columnTo . $row; + $cellStyle = $sheet->getStyle($range); + } else { + $cellStyle = $sheet->getStyle($column . $row); + } + + // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color + $styles = explode(';', $attributeArray['style']); + foreach ($styles as $st) { + $value = explode(':', $st); + $styleName = trim($value[0]); + $styleValue = isset($value[1]) ? trim($value[1]) : null; + $styleValueString = (string) $styleValue; + + if (!$styleName) { + continue; + } + + switch ($styleName) { + case 'background': + case 'background-color': + $styleColor = $this->getStyleColor($styleValueString); + + if (!$styleColor) { + continue 2; + } + + $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]); + + break; + case 'color': + $styleColor = $this->getStyleColor($styleValueString); + + if (!$styleColor) { + continue 2; + } + + $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]); + + break; + + case 'border': + $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders'); + + break; + + case 'border-top': + $this->setBorderStyle($cellStyle, $styleValueString, 'top'); + + break; + + case 'border-bottom': + $this->setBorderStyle($cellStyle, $styleValueString, 'bottom'); + + break; + + case 'border-left': + $this->setBorderStyle($cellStyle, $styleValueString, 'left'); + + break; + + case 'border-right': + $this->setBorderStyle($cellStyle, $styleValueString, 'right'); + + break; + + case 'font-size': + $cellStyle->getFont()->setSize( + (float) $styleValue + ); + + break; + + case 'direction': + if ($styleValue === 'rtl') { + $cellStyle->getAlignment() + ->setReadOrder(Alignment::READORDER_RTL); + } elseif ($styleValue === 'ltr') { + $cellStyle->getAlignment() + ->setReadOrder(Alignment::READORDER_LTR); + } + + break; + + case 'font-weight': + if ($styleValue === 'bold' || $styleValue >= 500) { + $cellStyle->getFont()->setBold(true); + } + + break; + + case 'font-style': + if ($styleValue === 'italic') { + $cellStyle->getFont()->setItalic(true); + } + + break; + + case 'font-family': + $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString)); + + break; + + case 'text-decoration': + switch ($styleValue) { + case 'underline': + $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + + break; + case 'line-through': + $cellStyle->getFont()->setStrikethrough(true); + + break; + } + + break; + + case 'text-align': + $cellStyle->getAlignment()->setHorizontal($styleValueString); + + break; + + case 'vertical-align': + $cellStyle->getAlignment()->setVertical($styleValueString); + + break; + + case 'width': + if ($column !== '') { + $sheet->getColumnDimension($column)->setWidth( + (new CssDimension($styleValue ?? ''))->width() + ); + } + + break; + + case 'height': + if ($row > 0) { + $sheet->getRowDimension($row)->setRowHeight( + (new CssDimension($styleValue ?? ''))->height() + ); + } + + break; + + case 'word-wrap': + $cellStyle->getAlignment()->setWrapText( + $styleValue === 'break-word' + ); + + break; + + case 'text-indent': + $indentDimension = new CssDimension($styleValueString); + $indent = $indentDimension + ->toUnit(CssDimension::UOM_PIXELS); + $cellStyle->getAlignment()->setIndent( + (int) ($indent / Alignment::INDENT_UNITS_TO_PIXELS) + ); + + break; + } + } + } + + /** + * Check if has #, so we can get clean hex. + */ + public function getStyleColor(?string $value): string + { + $value = (string) $value; + if (str_starts_with($value, '#')) { + return substr($value, 1); + } + + return HelperHtml::colourNameLookup($value); + } + + /** @param string[] $attributes */ + private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void + { + if (!isset($attributes['src'])) { + return; + } + $styleArray = self::getStyleArray($attributes); + + $src = $attributes['src']; + if (!str_starts_with($src, 'data:')) { + $src = urldecode($src); + } + $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null); + $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null); + $name = $attributes['alt'] ?? null; + + $drawing = new Drawing(); + $drawing->setPath($src, false, allowExternal: $this->allowExternalImages); + if ($drawing->getPath() === '') { + return; + } + $drawing->setWorksheet($sheet); + $drawing->setCoordinates($column . $row); + $drawing->setOffsetX(0); + $drawing->setOffsetY(10); + $drawing->setResizeProportional(true); + + if ($name) { + $drawing->setName($name); + } + + /** @var null|scalar $width */ + /** @var null|scalar $height */ + if ($width) { + if ($height) { + $drawing->setWidthAndHeight((int) $width, (int) $height); + } else { + $drawing->setWidth((int) $width); + } + } elseif ($height) { + $drawing->setHeight((int) $height); + } + + $sheet->getColumnDimension($column)->setWidth( + $drawing->getWidth() / 6 + ); + + $sheet->getRowDimension($row)->setRowHeight( + $drawing->getHeight() * 0.9 + ); + + if (isset($styleArray['opacity'])) { + $opacity = $styleArray['opacity']; + if (is_numeric($opacity)) { + $drawing->setOpacity((int) ($opacity * 100000)); + } + } + } + + /** + * @param string[] $attributes + * + * @return mixed[] + */ + private static function getStyleArray(array $attributes): array + { + $styleArray = []; + if (isset($attributes['style'])) { + $styles = explode(';', $attributes['style']); + foreach ($styles as $style) { + $value = explode(':', $style); + if (count($value) === 2) { + $arrayKey = trim($value[0]); + $arrayValue = trim($value[1]); + if ($arrayKey === 'width') { + if (str_ends_with($arrayValue, 'px')) { + $arrayValue = (string) (((float) substr($arrayValue, 0, -2))); + } else { + $arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS); + } + } elseif ($arrayKey === 'height') { + if (str_ends_with($arrayValue, 'px')) { + $arrayValue = substr($arrayValue, 0, -2); + } else { + $arrayValue = (new CssDimension($arrayValue))->toUnit(CssDimension::UOM_PIXELS); + } + } + $styleArray[$arrayKey] = $arrayValue; + } + } + } + + return $styleArray; + } + + private const BORDER_MAPPINGS = [ + 'dash-dot' => Border::BORDER_DASHDOT, + 'dash-dot-dot' => Border::BORDER_DASHDOTDOT, + 'dashed' => Border::BORDER_DASHED, + 'dotted' => Border::BORDER_DOTTED, + 'double' => Border::BORDER_DOUBLE, + 'hair' => Border::BORDER_HAIR, + 'medium' => Border::BORDER_MEDIUM, + 'medium-dashed' => Border::BORDER_MEDIUMDASHED, + 'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT, + 'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT, + 'none' => Border::BORDER_NONE, + 'slant-dash-dot' => Border::BORDER_SLANTDASHDOT, + 'solid' => Border::BORDER_THIN, + 'thick' => Border::BORDER_THICK, + ]; + + /** @return array */ + public static function getBorderMappings(): array + { + return self::BORDER_MAPPINGS; + } + + /** + * Map html border style to PhpSpreadsheet border style. + */ + public function getBorderStyle(string $style): ?string + { + return self::BORDER_MAPPINGS[$style] ?? null; + } + + private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void + { + if (trim($styleValue) === Border::BORDER_NONE) { + $borderStyle = Border::BORDER_NONE; + $color = null; + } else { + $borderArray = explode(' ', $styleValue); + $borderCount = count($borderArray); + if ($borderCount >= 3) { + $borderStyle = $borderArray[1]; + $color = $borderArray[2]; + } else { + $borderStyle = $borderArray[0]; + $color = $borderArray[1] ?? null; + } + } + + $cellStyle->applyFromArray([ + 'borders' => [ + $type => [ + 'borderStyle' => $this->getBorderStyle($borderStyle), + 'color' => ['rgb' => $this->getStyleColor($color)], + ], + ], + ]); + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + $info = []; + $spreadsheet = $this->newSpreadsheet(); + $this->loadIntoExisting($filename, $spreadsheet); + foreach ($spreadsheet->getAllSheets() as $sheet) { + $newEntry = ['worksheetName' => $sheet->getTitle()]; + $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn(); + $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1; + $newEntry['totalRows'] = $sheet->getHighestDataRow(); + $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1; + $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE; + $info[] = $newEntry; + } + $spreadsheet->disconnectWorksheets(); + + return $info; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/IReadFilter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/IReadFilter.php new file mode 100644 index 0000000..1fd12e5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/IReadFilter.php @@ -0,0 +1,15 @@ +securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + $mimeType = 'UNKNOWN'; + + // Load file + + if (File::testFileNoThrow($filename, '')) { + $zip = new ZipArchive(); + if ($zip->open($filename) === true) { + // check if it is an OOXML archive + $stat = $zip->statName('mimetype'); + if (!empty($stat) && ($stat['size'] <= 255)) { + $mimeType = $zip->getFromName($stat['name']); + } elseif ($zip->statName('META-INF/manifest.xml')) { + $xml = simplexml_load_string( + $this->getSecurityScannerOrThrow() + ->scan( + $zip->getFromName( + 'META-INF/manifest.xml' + ) + ) + ); + if ($xml !== false) { + $namespacesContent = $xml->getNamespaces(true); + if (isset($namespacesContent['manifest'])) { + $manifest = $xml->children($namespacesContent['manifest']); + foreach ($manifest as $manifestDataSet) { + $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']); + if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') { + $mimeType = (string) $manifestAttributes->{'media-type'}; + + break; + } + } + } + } + } + + $zip->close(); + } + } + + return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet'; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. + * + * @return string[] + */ + public function listWorksheetNames(string $filename): array + { + File::assertFile($filename, self::INITIAL_FILE); + + $worksheetNames = []; + + $xml = new XMLReader(); + $xml->xml( + $this->getSecurityScannerOrThrow() + ->scanFile( + 'zip://' . realpath($filename) . '#' . self::INITIAL_FILE + ) + ); + $xml->setParserProperty(2, true); + + // Step into the first level of content of the XML + $xml->read(); + while ($xml->read()) { + // Quickly jump through to the office:body node + while ($xml->name !== 'office:body') { + if ($xml->isEmptyElement) { + $xml->read(); + } else { + $xml->next(); + } + } + // Now read each node until we find our first table:table node + while ($xml->read()) { + $xmlName = $xml->name; + if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + // Loop through each table:table node reading the table:name attribute for each worksheet name + do { + $worksheetName = $xml->getAttribute('table:name'); + if (!empty($worksheetName)) { + $worksheetNames[] = $worksheetName; + } + $xml->next(); + } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); + } + } + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + File::assertFile($filename, self::INITIAL_FILE); + + $worksheetInfo = []; + + $xml = new XMLReader(); + $xml->xml( + $this->getSecurityScannerOrThrow() + ->scanFile( + 'zip://' . realpath($filename) . '#' . self::INITIAL_FILE + ) + ); + $xml->setParserProperty(2, true); + + // Step into the first level of content of the XML + $xml->read(); + $tableVisibility = []; + $lastTableStyle = ''; + + while ($xml->read()) { + if ($xml->name === 'style:style') { + $styleType = $xml->getAttribute('style:family'); + if ($styleType === 'table') { + $lastTableStyle = $xml->getAttribute('style:name'); + } + } elseif ($xml->name === 'style:table-properties') { + $visibility = $xml->getAttribute('table:display'); + $tableVisibility[$lastTableStyle] = ($visibility === 'false') ? Worksheet::SHEETSTATE_HIDDEN : Worksheet::SHEETSTATE_VISIBLE; + } elseif ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + $worksheetNames[] = $xml->getAttribute('table:name'); + + $styleName = $xml->getAttribute('table:style-name') ?? ''; + $visibility = $tableVisibility[$styleName] ?? ''; + $tmpInfo = [ + 'worksheetName' => (string) $xml->getAttribute('table:name'), + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + 'sheetState' => $visibility, + ]; + + // Loop through each child node of the table:table element reading + $currCells = 0; + do { + $xml->read(); + if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { + $rowspan = $xml->getAttribute('table:number-rows-repeated'); + $rowspan = empty($rowspan) ? 1 : (int) $rowspan; + $tmpInfo['totalRows'] += $rowspan; + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $currCells = 0; + // Step into the row + $xml->read(); + do { + $doread = true; + if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + if (!$xml->isEmptyElement) { + ++$currCells; + $xml->next(); + $doread = false; + } + } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + $mergeSize = $xml->getAttribute('table:number-columns-repeated'); + $currCells += (int) $mergeSize; + } + if ($doread) { + $xml->read(); + } + } while ($xml->name != 'table:table-row'); + } + } while ($xml->name != 'table:table'); + + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $worksheetInfo[] = $tmpInfo; + } + } + + return $worksheetInfo; + } + + /** + * Loads PhpSpreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + $spreadsheet->removeSheetByIndex(0); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + File::assertFile($filename, self::INITIAL_FILE); + + $zip = new ZipArchive(); + $zip->open($filename); + + // Meta + + $xml = @simplexml_load_string( + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName('meta.xml')) + ); + if ($xml === false) { + throw new Exception('Unable to read data from {$pFilename}'); + } + + /** @var array{meta?: string, office?: string, dc?: string} */ + $namespacesMeta = $xml->getNamespaces(true); + + (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta); + + // Styles + + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName('styles.xml')) + ); + + $pageSettings = new PageSettings($dom); + + // Main Content + + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName(self::INITIAL_FILE)) + ); + + $officeNs = (string) $dom->lookupNamespaceUri('office'); + $tableNs = (string) $dom->lookupNamespaceUri('table'); + $textNs = (string) $dom->lookupNamespaceUri('text'); + $xlinkNs = (string) $dom->lookupNamespaceUri('xlink'); + $styleNs = (string) $dom->lookupNamespaceUri('style'); + + $pageSettings->readStyleCrossReferences($dom); + + $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); + $definedNameReader = new DefinedNames($spreadsheet, $tableNs); + $columnWidths = []; + $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0); + $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style'); + foreach ($automaticStyles as $automaticStyle) { + $styleName = $automaticStyle->getAttributeNS($styleNs, 'name'); + $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family'); + if ($styleFamily === 'table-column') { + $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties'); + $tcprop = $tcprops->item(0); + if ($tcprop !== null) { + $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width'); + $columnWidths[$styleName] = $columnWidth; + } + } + } + + // Content + $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0); + $spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet'); + + foreach ($spreadsheets as $workbookData) { + /** @var DOMElement $workbookData */ + $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); + + $worksheetID = 0; + $sheetCreated = false; + foreach ($tables as $worksheetDataSet) { + /** @var DOMElement $worksheetDataSet */ + $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); + + // Check loadSheetsOnly + if ( + $this->loadSheetsOnly !== null + && $worksheetName + && !in_array($worksheetName, $this->loadSheetsOnly) + ) { + continue; + } + + $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name'); + + // Create sheet + $spreadsheet->createSheet(); + $sheetCreated = true; + $spreadsheet->setActiveSheetIndex($worksheetID); + + if ($worksheetName || is_numeric($worksheetName)) { + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in + // formula cells... during the load, all formulae should be correct, and we're simply + // bringing the worksheet name in line with the formula, not the reverse + $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false); + } + + // Go through every child of table element + $rowID = 1; + $tableColumnIndex = 1; + foreach ($worksheetDataSet->childNodes as $childNode) { + /** @var DOMElement $childNode */ + + // Filter elements which are not under the "table" ns + if ($childNode->namespaceURI != $tableNs) { + continue; + } + + $key = self::extractNodeName($childNode->nodeName); + + switch ($key) { + case 'table-header-rows': + case 'table-rows': + $this->processTableHeaderRows( + $childNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-row-group': + $this->processTableRowGroup( + $childNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-header-columns': + case 'table-columns': + $this->processTableHeaderColumns( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-column-group': + $this->processTableColumnGroup( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-column': + $this->processTableColumn( + $childNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-row': + $this->processTableRow( + $childNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + } + } + $pageSettings->setVisibilityForWorksheet( + $spreadsheet->getActiveSheet(), + $worksheetStyleName + ); + $pageSettings->setPrintSettingsForWorksheet( + $spreadsheet->getActiveSheet(), + $worksheetStyleName + ); + ++$worksheetID; + } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $spreadsheet->createSheet(); + } + + $autoFilterReader->read($workbookData); + $definedNameReader->read($workbookData); + } + $spreadsheet->setActiveSheetIndex(0); + + if ($zip->locateName('settings.xml') !== false) { + $this->processSettings($zip, $spreadsheet); + } + + // Return + return $spreadsheet; + } + + private function processTableHeaderRows( + DOMElement $childNode, + string $tableNs, + int &$rowID, + string $worksheetName, + string $officeNs, + string $textNs, + string $xlinkNs, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-row': + $this->processTableRow( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + } + } + } + + private function processTableRowGroup( + DOMElement $childNode, + string $tableNs, + int &$rowID, + string $worksheetName, + string $officeNs, + string $textNs, + string $xlinkNs, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-row': + $this->processTableRow( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-header-rows': + case 'table-rows': + $this->processTableHeaderRows( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + case 'table-row-group': + $this->processTableRowGroup( + $grandchildNode, + $tableNs, + $rowID, + $worksheetName, + $officeNs, + $textNs, + $xlinkNs, + $spreadsheet + ); + + break; + } + } + } + + private function processTableRow( + DOMElement $childNode, + string $tableNs, + int &$rowID, + string $worksheetName, + string $officeNs, + string $textNs, + string $xlinkNs, + Spreadsheet $spreadsheet + ): void { + if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); + } else { + $rowRepeats = 1; + } + + $columnID = 'A'; + /** @var DOMElement|DOMText $cellData */ + foreach ($childNode->childNodes as $cellData) { + if ($cellData instanceof DOMText) { + continue; // should just be whitespace + } + if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $colRepeats = 1; + } + + for ($i = 0; $i < $colRepeats; ++$i) { + StringHelper::stringIncrement($columnID); + } + + continue; + } + + // Initialize variables + $formatting = $hyperlink = null; + $hasCalculatedValue = false; + $cellDataFormula = ''; + $cellDataType = ''; + $cellDataRef = ''; + + if ($cellData->hasAttributeNS($tableNs, 'formula')) { + $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); + $hasCalculatedValue = true; + } + if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) { + if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) { + $cellDataType = 'array'; + $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned'); + $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned'); + $lastRow = $rowID + $arrayRow - 1; + $lastCol = $columnID; + while ($arrayCol > 1) { + StringHelper::stringIncrement($lastCol); + --$arrayCol; + } + $cellDataRef = "$columnID$rowID:$lastCol$lastRow"; + } + } + + // Annotations + $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); + + if ($annotation->length > 0 && $annotation->item(0) !== null) { + $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); + $textNodeLength = $textNode->length; + $newLineOwed = false; + for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) { + $textNodeItem = $textNode->item($textNodeIndex); + if ($textNodeItem !== null) { + $text = $this->scanElementForText($textNodeItem); + if ($newLineOwed) { + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText("\n"); + } + $newLineOwed = true; + + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText( + $this->parseRichText($text) + ); + } + } + } + + // Content + + /** @var DOMElement[] $paragraphs */ + $paragraphs = []; + + foreach ($cellData->childNodes as $item) { + /** @var DOMElement $item */ + + // Filter text:p elements + if ($item->nodeName == 'text:p') { + $paragraphs[] = $item; + } + } + + if (count($paragraphs) > 0) { + // Consolidate if there are multiple p records (maybe with spans as well) + $dataArray = []; + + // Text can have multiple text:p and within those, multiple text:span. + // text:p newlines, but text:span does not. + // Also, here we assume there is no text data is span fields are specified, since + // we have no way of knowing proper positioning anyway. + + foreach ($paragraphs as $pData) { + $dataArray[] = $this->scanElementForText($pData); + } + $allCellDataText = implode("\n", $dataArray); + + $type = $cellData->getAttributeNS($officeNs, 'value-type'); + + switch ($type) { + case 'string': + $type = DataType::TYPE_STRING; + $dataValue = $allCellDataText; + + foreach ($paragraphs as $paragraph) { + $link = $paragraph->getElementsByTagNameNS($textNs, 'a'); + if ($link->length > 0 && $link->item(0) !== null) { + $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); + } + } + + break; + case 'boolean': + $type = DataType::TYPE_BOOL; + $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false; + + break; + case 'percentage': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + // percentage should always be float + //if (floor($dataValue) == $dataValue) { + // $dataValue = (int) $dataValue; + //} + $formatting = NumberFormat::FORMAT_PERCENTAGE_00; + + break; + case 'currency': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + if (floor($dataValue) == $dataValue) { + $dataValue = (int) $dataValue; + } + $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER; + + break; + case 'float': + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + + if (floor($dataValue) == $dataValue) { + if ($dataValue == (int) $dataValue) { + $dataValue = (int) $dataValue; + } + } + + break; + case 'date': + $type = DataType::TYPE_NUMERIC; + $value = $cellData->getAttributeNS($officeNs, 'date-value'); + $dataValue = Date::convertIsoDate($value); + + if ($dataValue != floor($dataValue)) { + $formatting = NumberFormat::FORMAT_DATE_XLSX15 + . ' ' + . NumberFormat::FORMAT_DATE_TIME4; + } else { + $formatting = NumberFormat::FORMAT_DATE_XLSX15; + } + + break; + case 'time': + $type = DataType::TYPE_NUMERIC; + + $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); + + $dataValue = Date::PHPToExcel( + strtotime( + '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) + ) + ); + $formatting = NumberFormat::FORMAT_DATE_TIME4; + + break; + default: + $dataValue = null; + } + } else { + $type = DataType::TYPE_NULL; + $dataValue = null; + } + + if ($hasCalculatedValue) { + $type = DataType::TYPE_FORMULA; + $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); + $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); + } + + if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $colRepeats = 1; + } + + if ($type !== null) { // @phpstan-ignore-line + for ($i = 0; $i < $colRepeats; ++$i) { + if ($i > 0) { + StringHelper::stringIncrement($columnID); + } + + if ($type !== DataType::TYPE_NULL) { + for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { + $rID = $rowID + $rowAdjust; + + $cell = $spreadsheet->getActiveSheet() + ->getCell($columnID . $rID); + + // Set value + if ($hasCalculatedValue) { + $cell->setValueExplicit($cellDataFormula, $type); + if ($cellDataType === 'array') { + $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]); + } + } elseif ($type !== '' || $dataValue !== null) { + $cell->setValueExplicit($dataValue, $type); + } + + if ($hasCalculatedValue) { + $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC); + } + + // Set other properties + if ($formatting !== null) { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode($formatting); + } else { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_GENERAL); + } + + if ($hyperlink !== null) { + if ($hyperlink[0] === '#') { + $hyperlink = 'sheet://' . substr($hyperlink, 1); + } + $cell->getHyperlink() + ->setUrl($hyperlink); + } + } + } + } + } + + // Merged cells + $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet); + + StringHelper::stringIncrement($columnID); + } + $rowID += $rowRepeats; + } + + private static function extractNodeName(string $key): string + { + // Remove ns from node name + if (str_contains($key, ':')) { + $keyChunks = explode(':', $key); + $key = array_pop($keyChunks); + } + + return $key; + } + + /** + * @param string[] $columnWidths + */ + private function processTableHeaderColumns( + DOMElement $childNode, + string $tableNs, + array $columnWidths, + int &$tableColumnIndex, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-column': + $this->processTableColumn( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + } + } + } + + /** + * @param string[] $columnWidths + */ + private function processTableColumnGroup( + DOMElement $childNode, + string $tableNs, + array $columnWidths, + int &$tableColumnIndex, + Spreadsheet $spreadsheet + ): void { + foreach ($childNode->childNodes as $grandchildNode) { + /** @var DOMElement $grandchildNode */ + $grandkey = self::extractNodeName($grandchildNode->nodeName); + switch ($grandkey) { + case 'table-column': + $this->processTableColumn( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-header-columns': + case 'table-columns': + $this->processTableHeaderColumns( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + case 'table-column-group': + $this->processTableColumnGroup( + $grandchildNode, + $tableNs, + $columnWidths, + $tableColumnIndex, + $spreadsheet + ); + + break; + } + } + } + + /** + * @param string[] $columnWidths + */ + private function processTableColumn( + DOMElement $childNode, + string $tableNs, + array $columnWidths, + int &$tableColumnIndex, + Spreadsheet $spreadsheet + ): void { + if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $rowRepeats = 1; + } + $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); + if (isset($columnWidths[$tableStyleName])) { + $columnWidth = new HelperDimension($columnWidths[$tableStyleName]); + $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex); + for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) { + /** @var string $tableColumnString */ + $spreadsheet->getActiveSheet() + ->getColumnDimension($tableColumnString) + ->setWidth($columnWidth->toUnit('cm'), 'cm'); + StringHelper::stringIncrement($tableColumnString); + } + } + $tableColumnIndex += $rowRepeats; + } + + private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void + { + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->getSecurityScannerOrThrow() + ->scan($zip->getFromName('settings.xml')) + ); + $configNs = (string) $dom->lookupNamespaceUri('config'); + $officeNs = (string) $dom->lookupNamespaceUri('office'); + $settings = $dom->getElementsByTagNameNS($officeNs, 'settings') + ->item(0); + if ($settings !== null) { + $this->lookForActiveSheet($settings, $spreadsheet, $configNs); + $this->lookForSelectedCells($settings, $spreadsheet, $configNs); + } + } + + private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void + { + /** @var DOMElement $t */ + foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { + if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { + try { + $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? ''); + } catch (Throwable) { + // do nothing + } + + break; + } + } + } + + private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void + { + /** @var DOMElement $t */ + foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) { + if ($t->getAttributeNs($configNs, 'name') === 'Tables') { + foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) { + $setRow = $setCol = ''; + $wsname = $ws->getAttributeNs($configNs, 'name'); + foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) { + $attrName = $configItem->getAttributeNs($configNs, 'name'); + if ($attrName === 'CursorPositionX') { + $setCol = $configItem->nodeValue; + } + if ($attrName === 'CursorPositionY') { + $setRow = $configItem->nodeValue; + } + } + $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow"); + } + + break; + } + } + } + + private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void + { + if (is_numeric($setCol) && is_numeric($setRow)) { + $sheet = $spreadsheet->getSheetByName($wsname); + if ($sheet !== null) { + $sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]); + } + } + } + + /** + * Recursively scan element. + */ + protected function scanElementForText(DOMNode $element): string + { + $str = ''; + foreach ($element->childNodes as $child) { + /** @var DOMNode $child */ + if ($child->nodeType == XML_TEXT_NODE) { + $str .= $child->nodeValue; + } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') { + $str .= "\n"; + } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { + // It's a space + + // Multiple spaces? + $attributes = $child->attributes; + /** @var ?DOMAttr $cAttr */ + $cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c'); + $multiplier = self::getMultiplier($cAttr); + $str .= str_repeat(' ', $multiplier); + } + + if ($child->hasChildNodes()) { + $str .= $this->scanElementForText($child); + } + } + + return $str; + } + + private static function getMultiplier(?DOMAttr $cAttr): int + { + if ($cAttr) { + $multiplier = (int) $cAttr->nodeValue; + } else { + $multiplier = 1; + } + + return $multiplier; + } + + private function parseRichText(string $is): RichText + { + $value = new RichText(); + $value->createText($is); + + return $value; + } + + private function processMergedCells( + DOMElement $cellData, + string $tableNs, + string $type, + string $columnID, + int $rowID, + Spreadsheet $spreadsheet + ): void { + if ( + $cellData->hasAttributeNS($tableNs, 'number-columns-spanned') + || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned') + ) { + if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) { + $columnTo = $columnID; + + if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { + $columnIndex = Coordinate::columnIndexFromString($columnID); + $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); + $columnIndex -= 2; + + $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1); + } + + $rowTo = $rowID; + + if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { + $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; + } + + $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php new file mode 100644 index 0000000..1f5f975 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php @@ -0,0 +1,45 @@ +readAutoFilters($workbookData); + } + + protected function readAutoFilters(DOMElement $workbookData): void + { + $databases = $workbookData->getElementsByTagNameNS($this->tableNs, 'database-ranges'); + + foreach ($databases as $autofilters) { + foreach ($autofilters->childNodes as $autofilter) { + $autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address'); + if ($autofilterRange !== null) { + $baseAddress = FormulaTranslator::convertToExcelAddressValue($autofilterRange); + $this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress); + } + } + } + } + + protected function getAttributeValue(?DOMNode $node, string $attributeName): ?string + { + if ($node !== null && $node->attributes !== null) { + $attribute = $node->attributes->getNamedItemNS( + $this->tableNs, + $attributeName + ); + + if ($attribute !== null) { + return $attribute->nodeValue; + } + } + + return null; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php new file mode 100644 index 0000000..c280c4e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php @@ -0,0 +1,21 @@ +spreadsheet = $spreadsheet; + $this->tableNs = $tableNs; + } + + abstract public function read(DOMElement $workbookData): void; +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php new file mode 100644 index 0000000..713ea55 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -0,0 +1,70 @@ +readDefinedRanges($workbookData); + $this->readDefinedExpressions($workbookData); + } + + /** + * Read any Named Ranges that are defined in this spreadsheet. + */ + protected function readDefinedRanges(DOMElement $workbookData): void + { + $namedRanges = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-range'); + foreach ($namedRanges as $definedNameElement) { + $definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name'); + $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); + $range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address'); + + /** @var non-empty-string $baseAddress */ + $baseAddress = FormulaTranslator::convertToExcelAddressValue($baseAddress); + $range = FormulaTranslator::convertToExcelAddressValue($range); + + $this->addDefinedName($baseAddress, $definedName, $range); + } + } + + /** + * Read any Named Formulae that are defined in this spreadsheet. + */ + protected function readDefinedExpressions(DOMElement $workbookData): void + { + $namedExpressions = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-expression'); + foreach ($namedExpressions as $definedNameElement) { + $definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name'); + $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); + $expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression'); + + /** @var non-empty-string $baseAddress */ + $baseAddress = FormulaTranslator::convertToExcelAddressValue($baseAddress); + $expression = substr($expression, strpos($expression, ':=') + 1); + $expression = FormulaTranslator::convertToExcelFormulaValue($expression); + + $this->addDefinedName($baseAddress, $definedName, $expression); + } + } + + /** + * Assess scope and store the Defined Name. + * + * @param non-empty-string $baseAddress + */ + private function addDefinedName(string $baseAddress, string $definedName, string $value): void + { + [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true, true); + $worksheet = $this->spreadsheet->getSheetByName($sheetReference); + // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet + if ($worksheet !== null) { + $this->spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value)); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php new file mode 100644 index 0000000..032c04a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php @@ -0,0 +1,127 @@ +setDomNameSpaces($styleDom); + $this->readPageSettingStyles($styleDom); + $this->readStyleMasterLookup($styleDom); + } + + private function setDomNameSpaces(DOMDocument $styleDom): void + { + $this->officeNs = (string) $styleDom->lookupNamespaceUri('office'); + $this->stylesNs = (string) $styleDom->lookupNamespaceUri('style'); + $this->stylesFo = (string) $styleDom->lookupNamespaceUri('fo'); + $this->tableNs = (string) $styleDom->lookupNamespaceUri('table'); + } + + private function readPageSettingStyles(DOMDocument $styleDom): void + { + $item0 = $styleDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles')->item(0); + $styles = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($this->stylesNs, 'page-layout'); + + foreach ($styles as $styleSet) { + $styleName = $styleSet->getAttributeNS($this->stylesNs, 'name'); + $pageLayoutProperties = $styleSet->getElementsByTagNameNS($this->stylesNs, 'page-layout-properties')->item(0); + $styleOrientation = $pageLayoutProperties?->getAttributeNS($this->stylesNs, 'print-orientation'); + $styleScale = $pageLayoutProperties?->getAttributeNS($this->stylesNs, 'scale-to'); + $stylePrintOrder = $pageLayoutProperties?->getAttributeNS($this->stylesNs, 'print-page-order'); + $centered = $pageLayoutProperties?->getAttributeNS($this->stylesNs, 'table-centering'); + + $marginLeft = $pageLayoutProperties?->getAttributeNS($this->stylesFo, 'margin-left'); + $marginRight = $pageLayoutProperties?->getAttributeNS($this->stylesFo, 'margin-right'); + $marginTop = $pageLayoutProperties?->getAttributeNS($this->stylesFo, 'margin-top'); + $marginBottom = $pageLayoutProperties?->getAttributeNS($this->stylesFo, 'margin-bottom'); + $header = $styleSet->getElementsByTagNameNS($this->stylesNs, 'header-style')->item(0); + $headerProperties = $header?->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')?->item(0); + $marginHeader = $headerProperties?->getAttributeNS($this->stylesFo, 'min-height'); + $footer = $styleSet->getElementsByTagNameNS($this->stylesNs, 'footer-style')->item(0); + $footerProperties = $footer?->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')?->item(0); + $marginFooter = $footerProperties?->getAttributeNS($this->stylesFo, 'min-height'); + + $this->pageLayoutStyles[$styleName] = (object) [ + 'orientation' => $styleOrientation ?: PageSetup::ORIENTATION_DEFAULT, + 'scale' => $styleScale ?: 100, + 'printOrder' => $stylePrintOrder, + 'horizontalCentered' => $centered === 'horizontal' || $centered === 'both', + 'verticalCentered' => $centered === 'vertical' || $centered === 'both', + // margin size is already stored in inches, so no UOM conversion is required + 'marginLeft' => (float) ($marginLeft ?? 0.7), + 'marginRight' => (float) ($marginRight ?? 0.7), + 'marginTop' => (float) ($marginTop ?? 0.3), + 'marginBottom' => (float) ($marginBottom ?? 0.3), + 'marginHeader' => (float) ($marginHeader ?? 0.45), + 'marginFooter' => (float) ($marginFooter ?? 0.45), + ]; + } + } + + private function readStyleMasterLookup(DOMDocument $styleDom): void + { + $item0 = $styleDom->getElementsByTagNameNS($this->officeNs, 'master-styles')->item(0); + $styleMasterLookup = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($this->stylesNs, 'master-page'); + + foreach ($styleMasterLookup as $styleMasterSet) { + $styleMasterName = $styleMasterSet->getAttributeNS($this->stylesNs, 'name'); + $pageLayoutName = $styleMasterSet->getAttributeNS($this->stylesNs, 'page-layout-name'); + $this->masterPrintStylesCrossReference[$styleMasterName] = $pageLayoutName; + } + } + + public function readStyleCrossReferences(DOMDocument $contentDom): void + { + $item0 = $contentDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles')->item(0); + $styleXReferences = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($this->stylesNs, 'style'); + + foreach ($styleXReferences as $styleXreferenceSet) { + $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); + $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + $styleFamilyName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'family'); + if (!empty($styleFamilyName) && $styleFamilyName === 'table') { + $styleVisibility = 'true'; + foreach ($styleXreferenceSet->getElementsByTagNameNS($this->stylesNs, 'table-properties') as $tableProperties) { + $styleVisibility = $tableProperties->getAttributeNS($this->tableNs, 'display'); + } + $this->tableStylesCrossReference[$styleXRefName] = $styleVisibility; + } + if (!empty($stylePageLayoutName)) { + $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; + } + } + } + + public function setVisibilityForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->tableStylesCrossReference)) { + return; + } + + $worksheet->setSheetState( + $this->tableStylesCrossReference[$styleName] === 'false' + ? Worksheet::SHEETSTATE_HIDDEN + : Worksheet::SHEETSTATE_VISIBLE + ); + } + + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { + return; + } + $masterStyleName = $this->masterStylesCrossReference[$styleName]; + + if (!array_key_exists($masterStyleName, $this->masterPrintStylesCrossReference)) { + return; + } + $printSettingsIndex = $this->masterPrintStylesCrossReference[$masterStyleName]; + + if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) { + return; + } + /** @var (object{orientation: string, scale: int|string, printOrder: ?string, + * horizontalCentered: bool, verticalCentered: bool, marginLeft: float, marginRight: float, marginTop: float, + * marginBottom: float, marginHeader: float, marginFooter: float}&stdClass) */ + $printSettings = $this->pageLayoutStyles[$printSettingsIndex]; + + $worksheet->getPageSetup() + ->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT) + ->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER) + ->setScale((int) trim((string) $printSettings->scale, '%')) + ->setHorizontalCentered($printSettings->horizontalCentered) + ->setVerticalCentered($printSettings->verticalCentered); + + $worksheet->getPageMargins() + ->setLeft($printSettings->marginLeft) + ->setRight($printSettings->marginRight) + ->setTop($printSettings->marginTop) + ->setBottom($printSettings->marginBottom) + ->setHeader($printSettings->marginHeader) + ->setFooter($printSettings->marginFooter); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/Properties.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/Properties.php new file mode 100644 index 0000000..4dada28 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Ods/Properties.php @@ -0,0 +1,141 @@ +spreadsheet = $spreadsheet; + } + + /** @param array{meta?: string, office?: string, dc?: string} $namespacesMeta */ + public function load(SimpleXMLElement $xml, array $namespacesMeta): void + { + $docProps = $this->spreadsheet->getProperties(); + $officeProperty = $xml->children($namespacesMeta['office'] ?? ''); + foreach ($officeProperty as $officePropertyData) { + if (isset($namespacesMeta['dc'])) { + $officePropertiesDC = $officePropertyData->children($namespacesMeta['dc']); + $this->setCoreProperties($docProps, $officePropertiesDC); + } + + $officePropertyMeta = null; + if (isset($namespacesMeta['dc'])) { + $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta'] ?? ''); + } + $officePropertyMeta = $officePropertyMeta ?? []; + foreach ($officePropertyMeta as $propertyName => $propertyValue) { + $this->setMetaProperties($namespacesMeta, $propertyValue, $propertyName, $docProps); + } + } + } + + private function setCoreProperties(DocumentProperties $docProps, SimpleXMLElement $officePropertyDC): void + { + foreach ($officePropertyDC as $propertyName => $propertyValue) { + $propertyValue = (string) $propertyValue; + switch ($propertyName) { + case 'title': + $docProps->setTitle($propertyValue); + + break; + case 'subject': + $docProps->setSubject($propertyValue); + + break; + case 'creator': + $docProps->setCreator($propertyValue); + $docProps->setLastModifiedBy($propertyValue); + + break; + case 'date': + $docProps->setModified($propertyValue); + + break; + case 'description': + $docProps->setDescription($propertyValue); + + break; + } + } + } + + /** @param array{meta?: string, office?: mixed, dc?: mixed} $namespacesMeta */ + private function setMetaProperties( + array $namespacesMeta, + SimpleXMLElement $propertyValue, + string $propertyName, + DocumentProperties $docProps + ): void { + $propertyValueAttributes = $propertyValue->attributes($namespacesMeta['meta'] ?? ''); + $propertyValue = (string) $propertyValue; + switch ($propertyName) { + case 'initial-creator': + $docProps->setCreator($propertyValue); + + break; + case 'keyword': + $docProps->setKeywords($propertyValue); + + break; + case 'creation-date': + $docProps->setCreated($propertyValue); + + break; + case 'user-defined': + $name2 = (string) ($propertyValueAttributes['name'] ?? ''); + if ($name2 === 'Company') { + $docProps->setCompany($propertyValue); + } elseif ($name2 === 'category') { + $docProps->setCategory($propertyValue); + } else { + $this->setUserDefinedProperty($propertyValueAttributes, $propertyValue, $docProps); + } + + break; + } + } + + /** @param iterable $propertyValueAttributes */ + private function setUserDefinedProperty(iterable $propertyValueAttributes, string $propertyValue, DocumentProperties $docProps): void + { + $propertyValueName = ''; + $propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING; + foreach ($propertyValueAttributes as $key => $value) { + if ($key == 'name') { + /** @var scalar $value */ + $propertyValueName = (string) $value; + } elseif ($key == 'value-type') { + /** @var string $value */ + switch ($value) { + case 'date': + $propertyValue = DocumentProperties::convertProperty($propertyValue, 'date'); + $propertyValueType = DocumentProperties::PROPERTY_TYPE_DATE; + + break; + case 'boolean': + $propertyValue = DocumentProperties::convertProperty($propertyValue, 'bool'); + $propertyValueType = DocumentProperties::PROPERTY_TYPE_BOOLEAN; + + break; + case 'float': + $propertyValue = DocumentProperties::convertProperty($propertyValue, 'r4'); + $propertyValueType = DocumentProperties::PROPERTY_TYPE_FLOAT; + + break; + default: + $propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING; + } + } + } + + $docProps->setCustomProperty($propertyValueName, $propertyValue, $propertyValueType); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Security/XmlScanner.php new file mode 100644 index 0000000..7b4e104 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -0,0 +1,117 @@ +pattern = $pattern; + } + + public static function getInstance(Reader\IReader $reader): self + { + $pattern = ($reader instanceof Reader\Html) ? 'callback = $callback; + } + + private static function forceString(mixed $arg): string + { + return is_string($arg) ? $arg : ''; + } + + private function toUtf8(string $xml): string + { + $charset = $this->findCharSet($xml); + $foundUtf7 = $charset === 'UTF-7'; + if ($charset !== 'UTF-8') { + $testStart = '/^.{0,4}\s*pattern, 1, 'UTF-8')) . '\0*/'; + + $xml = "$xml"; + if (preg_match($pattern, $xml)) { + throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); + } + + $xml = $this->toUtf8($xml); + if (preg_match($pattern, $xml)) { + throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); + } + + if ($this->callback !== null) { + $xml = call_user_func($this->callback, $xml); + } + /** @var string $xml */ + + return $xml; + } + + /** + * Scan the XML for use of scan(file_get_contents($filestream)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Slk.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Slk.php new file mode 100644 index 0000000..73d3738 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Slk.php @@ -0,0 +1,593 @@ +openFile($filename); + } catch (ReaderException) { + return false; + } + + // Read sample data (first 2 KB will do) + $data = (string) fread($this->fileHandle, 2048); + + // Count delimiters in file + $delimiterCount = substr_count($data, ';'); + $hasDelimiter = $delimiterCount > 0; + + // Analyze first line looking for ID; signature + $lines = explode("\n", $data); + $hasId = str_starts_with($lines[0], 'ID;P'); + + fclose($this->fileHandle); + + return $hasDelimiter && $hasId; + } + + private function canReadOrBust(string $filename): void + { + if (!$this->canRead($filename)) { + throw new ReaderException($filename . ' is an Invalid SYLK file.'); + } + $this->openFile($filename); + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + // Open file + $this->canReadOrBust($filename); + $fileHandle = $this->fileHandle; + rewind($fileHandle); + + $worksheetInfo = [['worksheetName' => basename($filename, '.slk')]]; + + // loop through one row (line) at a time in the file + $rowIndex = 0; + $columnIndex = 0; + while (($rowData = fgets($fileHandle)) !== false) { + $columnIndex = 0; + + // convert SYLK encoded $rowData to UTF-8 + $rowData = StringHelper::SYLKtoUTF8($rowData); + + // explode each row at semicolons while taking into account that literal semicolon (;) + // is escaped like this (;;) + $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData))))); + + $dataType = array_shift($rowData); + if ($dataType == 'B') { + foreach ($rowData as $rowDatum) { + switch ($rowDatum[0]) { + case 'X': + $columnIndex = (int) substr($rowDatum, 1) - 1; + + break; + case 'Y': + $rowIndex = (int) substr($rowDatum, 1); + + break; + } + } + + break; + } + } + + $worksheetInfo[0]['lastColumnIndex'] = $columnIndex; + $worksheetInfo[0]['totalRows'] = $rowIndex; + $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); + $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1; + $worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE; + + // Close file + fclose($fileHandle); + + return $worksheetInfo; + } + + /** + * Loads PhpSpreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + private const COLOR_ARRAY = [ + 'FF00FFFF', // 0 - cyan + 'FF000000', // 1 - black + 'FFFFFFFF', // 2 - white + 'FFFF0000', // 3 - red + 'FF00FF00', // 4 - green + 'FF0000FF', // 5 - blue + 'FFFFFF00', // 6 - yellow + 'FFFF00FF', // 7 - magenta + ]; + + private const FONT_STYLE_MAPPINGS = [ + 'B' => 'bold', + 'I' => 'italic', + 'U' => 'underline', + ]; + + /** + * @param-out true $hasCalculatedValue + */ + private function processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column): void + { + $cellDataFormula = '=' . substr($rowDatum, 1); + // Convert R1C1 style references to A1 style references (but only when not quoted) + $temp = explode('"', $cellDataFormula); + $key = false; + foreach ($temp as &$value) { + // Only count/replace in alternate array entries + $key = $key === false; + if ($key) { + preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); + // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way + // through the formula from left to right. Reversing means that we work right to left.through + // the formula + $cellReferences = array_reverse($cellReferences); + // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, + // then modify the formula to use that new reference + foreach ($cellReferences as $cellReference) { + $rowReference = $cellReference[2][0]; + // Empty R reference is the current row + if ($rowReference == '') { + $rowReference = $row; + } + // Bracketed R references are relative to the current row + if ($rowReference[0] == '[') { + $rowReference = (int) $row + (int) trim($rowReference, '[]'); + } + $columnReference = $cellReference[4][0]; + // Empty C reference is the current column + if ($columnReference == '') { + $columnReference = $column; + } + // Bracketed C references are relative to the current column + if ($columnReference[0] == '[') { + $columnReference = (int) $column + (int) trim($columnReference, '[]'); + } + $A1CellReference = Coordinate::stringFromColumnIndex((int) $columnReference) . $rowReference; + + $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); + } + } + } + unset($value); + // Then rebuild the formula string + $cellDataFormula = implode('"', $temp); + $hasCalculatedValue = true; + } + + /** @param mixed[] $rowData */ + private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void + { + // Read cell value data + $hasCalculatedValue = false; + $tryNumeric = false; + $cellDataFormula = $cellData = ''; + $sharedColumn = $sharedRow = -1; + $sharedFormula = false; + foreach ($rowData as $rowDatum) { + /** @var string $rowDatum */ + switch ($rowDatum[0]) { + case 'X': + $column = substr($rowDatum, 1); + + break; + case 'Y': + $row = substr($rowDatum, 1); + + break; + case 'K': + $cellData = substr($rowDatum, 1); + $tryNumeric = is_numeric($cellData); + + break; + case 'E': + $this->processFormula($rowDatum, $hasCalculatedValue, $cellDataFormula, $row, $column); + + break; + case 'A': + $comment = substr($rowDatum, 1); + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + $spreadsheet->getActiveSheet() + ->getComment("$columnLetter$row") + ->getText() + ->createText($comment); + + break; + case 'C': + $sharedColumn = (int) substr($rowDatum, 1); + + break; + case 'R': + $sharedRow = (int) substr($rowDatum, 1); + + break; + case 'S': + $sharedFormula = true; + + break; + } + } + if ($sharedFormula === true && $sharedRow >= 0 && $sharedColumn >= 0) { + $thisCoordinate = Coordinate::stringFromColumnIndex((int) $column) . $row; + $sharedCoordinate = Coordinate::stringFromColumnIndex($sharedColumn) . $sharedRow; + /** @var string */ + $formula = $spreadsheet->getActiveSheet()->getCell($sharedCoordinate)->getValue(); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($formula); + $referenceHelper = ReferenceHelper::getInstance(); + $newFormula = $referenceHelper->updateFormulaReferences($formula, 'A1', (int) $column - $sharedColumn, (int) $row - $sharedRow, '', true, false); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($newFormula); + //$calc = $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->getCalculatedValue(); + //$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($calc); + $cellData = Calculation::unwrapResult($cellData); + $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($cellData, $tryNumeric); + + return; + } + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + /** @var string */ + $cellData = Calculation::unwrapResult($cellData); + + // Set cell value + $this->processCFinal($spreadsheet, $hasCalculatedValue, $cellDataFormula, $cellData, "$columnLetter$row", $tryNumeric); + } + + private function processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate, bool $tryNumeric): void + { + // Set cell value + $spreadsheet->getActiveSheet()->getCell($coordinate)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData); + if ($hasCalculatedValue) { + $cellData = Calculation::unwrapResult($cellData); + $spreadsheet->getActiveSheet()->getCell($coordinate)->setCalculatedValue($cellData, $tryNumeric); + } + } + + /** @param mixed[] $rowData */ + private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void + { + // Read cell formatting + $formatStyle = $columnWidth = ''; + $startCol = $endCol = ''; + $fontStyle = ''; + $styleData = []; + foreach ($rowData as $rowDatum) { + /** @var string $rowDatum */ + switch ($rowDatum[0]) { + case 'C': + case 'X': + $column = substr($rowDatum, 1); + + break; + case 'R': + case 'Y': + $row = substr($rowDatum, 1); + + break; + case 'P': + $formatStyle = $rowDatum; + + break; + case 'W': + [$startCol, $endCol, $columnWidth] = explode(' ', substr($rowDatum, 1)); + + break; + case 'S': + $this->styleSettings($rowDatum, $styleData, $fontStyle); + + break; + } + } + /** @var string $formatStyle */ + $this->addFormats($spreadsheet, $formatStyle, $row, $column); + $this->addFonts($spreadsheet, $fontStyle, $row, $column); + $this->addStyle($spreadsheet, $styleData, $row, $column); + $this->addWidth($spreadsheet, $columnWidth, $startCol, $endCol); + } + + private const STYLE_SETTINGS_FONT = ['D' => 'bold', 'I' => 'italic']; + + private const STYLE_SETTINGS_BORDER = [ + 'B' => 'bottom', + 'L' => 'left', + 'R' => 'right', + 'T' => 'top', + ]; + + /** @param mixed[][] $styleData */ + private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void + { + $styleSettings = substr($rowDatum, 1); + $iMax = strlen($styleSettings); + for ($i = 0; $i < $iMax; ++$i) { + $char = $styleSettings[$i]; + if (array_key_exists($char, self::STYLE_SETTINGS_FONT)) { + $styleData['font'][self::STYLE_SETTINGS_FONT[$char]] = true; + } elseif (array_key_exists($char, self::STYLE_SETTINGS_BORDER)) { + $styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN; //* @phpstan-ignore-line + } elseif ($char == 'S') { + $styleData['fill']['fillType'] = Fill::FILL_PATTERN_GRAY125; + } elseif ($char == 'M') { + if (preg_match('/M([1-9]\d*)/', $styleSettings, $matches)) { + $fontStyle = $matches[1]; + } + } + } + } + + private function addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column): void + { + if ($formatStyle && $column > '' && $row > '') { + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + if (isset($this->formats[$formatStyle]) && is_array($this->formats[$formatStyle])) { + $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]); + } + } + } + + private function addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column): void + { + if ($fontStyle && $column > '' && $row > '') { + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + if (isset($this->fonts[$fontStyle]) && is_array($this->fonts[$fontStyle])) { + $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]); + } + } + } + + /** @param mixed[] $styleData */ + private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void + { + if ((!empty($styleData)) && $column > '' && $row > '') { + $columnLetter = Coordinate::stringFromColumnIndex((int) $column); + $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData); + } + } + + private function addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol): void + { + if ($columnWidth > '') { + if ($startCol == $endCol) { + $startCol = Coordinate::stringFromColumnIndex((int) $startCol); + $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth); + } else { + $startCol = Coordinate::stringFromColumnIndex((int) $startCol); + $endCol = Coordinate::stringFromColumnIndex((int) $endCol); + $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth); + do { + /** @var string $startCol */ + $spreadsheet->getActiveSheet() + ->getColumnDimension( + StringHelper::stringIncrement($startCol) + ) + ->setWidth((float) $columnWidth); + } while ($startCol !== $endCol); + } + } + } + + /** @param string[] $rowData */ + private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void + { + // Read shared styles + $formatArray = []; + $fromFormats = ['\-', '\ ']; + $toFormats = ['-', ' ']; + foreach ($rowData as $rowDatum) { + switch ($rowDatum[0]) { + case 'P': + $formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1)); + + break; + case 'E': + case 'F': + $formatArray['font']['name'] = substr($rowDatum, 1); + + break; + case 'M': + $formatArray['font']['size'] = ((float) substr($rowDatum, 1)) / 20; + + break; + case 'L': + /** @var mixed[][][] $formatArray */ + $this->processPColors($rowDatum, $formatArray); + + break; + case 'S': + $this->processPFontStyles($rowDatum, $formatArray); + + break; + } + } + $this->processPFinal($spreadsheet, $formatArray); + } + + /** @param mixed[][][] $formatArray */ + private function processPColors(string $rowDatum, array &$formatArray): void + { + if (preg_match('/L([1-9]\d*)/', $rowDatum, $matches)) { + $fontColor = ((int) $matches[1]) % 8; + $formatArray['font']['color']['argb'] = self::COLOR_ARRAY[$fontColor]; + } + } + + /** @param mixed[][] $formatArray */ + private function processPFontStyles(string $rowDatum, array &$formatArray): void + { + $styleSettings = substr($rowDatum, 1); + $iMax = strlen($styleSettings); + for ($i = 0; $i < $iMax; ++$i) { + if (array_key_exists($styleSettings[$i], self::FONT_STYLE_MAPPINGS)) { + $formatArray['font'][self::FONT_STYLE_MAPPINGS[$styleSettings[$i]]] = true; + } + } + } + + /** @param mixed[] $formatArray */ + private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void + { + if (array_key_exists('numberFormat', $formatArray)) { + $this->formats['P' . $this->format] = $formatArray; + ++$this->format; + } elseif (array_key_exists('font', $formatArray)) { + ++$this->fontcount; + $this->fonts[$this->fontcount] = $formatArray; + if ($this->fontcount === 1) { + $spreadsheet->getDefaultStyle()->applyFromArray($formatArray); + } + } + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + // Open file + $this->canReadOrBust($filename); + $fileHandle = $this->fileHandle; + rewind($fileHandle); + + // Create new Worksheets + while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { + $spreadsheet->createSheet(); + } + $spreadsheet->setActiveSheetIndex($this->sheetIndex); + $spreadsheet->getActiveSheet()->setTitle(substr(basename($filename, '.slk'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH)); + + // Loop through file + $column = $row = ''; + + // loop through one row (line) at a time in the file + while (($rowDataTxt = fgets($fileHandle)) !== false) { + // convert SYLK encoded $rowData to UTF-8 + $rowDataTxt = StringHelper::SYLKtoUTF8($rowDataTxt); + + // explode each row at semicolons while taking into account that literal semicolon (;) + // is escaped like this (;;) + $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowDataTxt))))); + + $dataType = array_shift($rowData); + if ($dataType == 'P') { + // Read shared styles + $this->processPRecord($rowData, $spreadsheet); + } elseif ($dataType == 'C') { + // Read cell value data + $this->processCRecord($rowData, $spreadsheet, $row, $column); + } elseif ($dataType == 'F') { + // Read cell formatting + $this->processFRecord($rowData, $spreadsheet, $row, $column); + } else { + $this->columnRowFromRowData($rowData, $column, $row); + } + } + + // Close file + fclose($fileHandle); + + // Return + return $spreadsheet; + } + + /** @param string[] $rowData */ + private function columnRowFromRowData(array $rowData, string &$column, string &$row): void + { + foreach ($rowData as $rowDatum) { + $char0 = $rowDatum[0]; + if ($char0 === 'X' || $char0 == 'C') { + $column = substr($rowDatum, 1); + } elseif ($char0 === 'Y' || $char0 == 'R') { + $row = substr($rowDatum, 1); + } + } + } + + /** + * Get sheet index. + */ + public function getSheetIndex(): int + { + return $this->sheetIndex; + } + + /** + * Set sheet index. + * + * @param int $sheetIndex Sheet index + * + * @return $this + */ + public function setSheetIndex(int $sheetIndex): static + { + $this->sheetIndex = $sheetIndex; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls.php new file mode 100644 index 0000000..09a2b77 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls.php @@ -0,0 +1,4903 @@ +data. + */ + protected int $dataSize; + + /** + * Current position in stream. + */ + protected int $pos; + + /** + * Workbook to be returned by the reader. + */ + protected Spreadsheet $spreadsheet; + + /** + * Worksheet that is currently being built by the reader. + */ + protected Worksheet $phpSheet; + + /** + * BIFF version. + */ + protected int $version = 0; + + /** + * Shared formats. + * + * @var mixed[] + */ + protected array $formats; + + /** + * Shared fonts. + * + * @var Font[] + */ + protected array $objFonts; + + /** + * Color palette. + * + * @var string[][] + */ + protected array $palette; + + /** + * Worksheets. + * + * @var array + */ + protected array $sheets; + + /** + * External books. + * + * @var mixed[][] + */ + protected array $externalBooks; + + /** + * REF structures. Only applies to BIFF8. + * + * @var array + */ + protected array $ref; + + /** + * External names. + * + * @var array|string> + */ + protected array $externalNames; + + /** + * Defined names. + * + * @var array{isBuiltInName: int, name: string, formula: string, scope: int} + */ + protected array $definedname; + + /** + * Shared strings. Only applies to BIFF8. + * + * @var array + */ + protected array $sst; + + /** + * Panes are frozen? (in sheet currently being read). See WINDOW2 record. + */ + protected bool $frozen; + + /** + * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record. + */ + protected bool $isFitToPages; + + /** + * Objects. One OBJ record contributes with one entry. + * + * @var mixed[] + */ + protected array $objs; + + /** + * Text Objects. One TXO record corresponds with one entry. + * + * @var array + */ + protected array $textObjects; + + /** + * Cell Annotations (BIFF8). + * + * @var mixed[] + */ + protected array $cellNotes; + + /** + * The combined MSODRAWINGGROUP data. + */ + protected string $drawingGroupData; + + /** + * The combined MSODRAWING data (per sheet). + */ + protected string $drawingData; + + /** + * Keep track of XF index. + */ + protected int $xfIndex; + + /** + * Mapping of XF index (that is a cell XF) to final index in cellXf collection. + * + * @var int[] + */ + protected array $mapCellXfIndex; + + /** + * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection. + * + * @var int[] + */ + protected array $mapCellStyleXfIndex; + + /** + * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value. + * + * @var mixed[] + */ + protected array $sharedFormulas; + + /** + * The shared formula parts in a sheet. One FORMULA record contributes with one value if it + * refers to a shared formula. + * + * @var mixed[] + */ + protected array $sharedFormulaParts; + + /** + * The type of encryption in use. + */ + protected int $encryption = 0; + + /** + * The position in the stream after which contents are encrypted. + */ + protected int $encryptionStartPos = 0; + + protected string $encryptionPassword = 'VelvetSweatshop'; + + /** + * The current RC4 decryption object. + */ + protected ?Xls\RC4 $rc4Key = null; + + /** + * The position in the stream that the RC4 decryption object was left at. + */ + protected int $rc4Pos = 0; + + /** + * The current MD5 context state. + * It is set via call-by-reference to verifyPassword. + */ + private string $md5Ctxt = ''; + + protected int $textObjRef; + + protected string $baseCell; + + protected bool $activeSheetSet = false; + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. + * + * @return string[] + */ + public function listWorksheetNames(string $filename): array + { + return (new Xls\ListFunctions())->listWorksheetNames2($filename, $this); + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this); + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetDimensions(string $filename): array + { + return (new Xls\ListFunctions())->listWorksheetDimensions2($filename, $this); + } + + /** + * Loads PhpSpreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + return (new Xls\LoadSpreadsheet())->loadSpreadsheetFromFile2($filename, $this); + } + + /** + * Read record data from stream, decrypting as required. + * + * @param string $data Data stream to read from + * @param int $pos Position to start reading from + * @param int $len Record data length + * + * @return string Record data + */ + protected function readRecordData(string $data, int $pos, int $len): string + { + $data = substr($data, $pos, $len); + + // File not encrypted, or record before encryption start point + if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) { + return $data; + } + + $recordData = ''; + if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) { + $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK); + $block = (int) floor($pos / self::REKEY_BLOCK); + $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK); + + // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting + // at a point earlier in the current block, re-use it as we can save some time. + if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) { + $this->rc4Key = $this->makeKey($block, $this->md5Ctxt); + $step = $pos % self::REKEY_BLOCK; + } else { + $step = $pos - $this->rc4Pos; + } + $this->rc4Key->RC4(str_repeat("\0", $step)); + + // Decrypt record data (re-keying at the end of every block) + while ($block != $endBlock) { + $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK); + $recordData .= $this->rc4Key->RC4(substr($data, 0, $step)); + $data = substr($data, $step); + $pos += $step; + $len -= $step; + ++$block; + $this->rc4Key = $this->makeKey($block, $this->md5Ctxt); + } + $recordData .= $this->rc4Key->RC4(substr($data, 0, $len)); + + // Keep track of the position of this decryptor. + // We'll try and re-use it later if we can to speed things up + $this->rc4Pos = $pos + $len; + } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) { + throw new Exception('XOr encryption not supported'); + } + + return $recordData; + } + + /** + * Use OLE reader to extract the relevant data streams from the OLE file. + */ + protected function loadOLE(string $filename): void + { + // OLE reader + $ole = new OLERead(); + // get excel data, + $ole->read($filename); + // Get workbook data: workbook stream + sheet streams + $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line + // Get summary information data + $this->summaryInformation = $ole->getStream($ole->summaryInformation); + // Get additional document summary information data + $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation); + } + + /** + * Read summary information. + */ + protected function readSummaryInformation(): void + { + if (!isset($this->summaryInformation)) { + return; + } + + // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark) + // offset: 2; size: 2; + // offset: 4; size: 2; OS version + // offset: 6; size: 2; OS indicator + // offset: 8; size: 16 + // offset: 24; size: 4; section count + //$secCount = self::getInt4d($this->summaryInformation, 24); + + // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9 + // offset: 44; size: 4 + $secOffset = self::getInt4d($this->summaryInformation, 44); + + // section header + // offset: $secOffset; size: 4; section length + //$secLength = self::getInt4d($this->summaryInformation, $secOffset); + + // offset: $secOffset+4; size: 4; property count + $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4); + + // initialize code page (used to resolve string values) + $codePage = 'CP1252'; + + // offset: ($secOffset+8); size: var + // loop through property decarations and properties + for ($i = 0; $i < $countProperties; ++$i) { + // offset: ($secOffset+8) + (8 * $i); size: 4; property ID + $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i)); + + // Use value of property id as appropriate + // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48) + $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i)); + + $type = self::getInt4d($this->summaryInformation, $secOffset + $offset); + + // initialize property value + $value = null; + + // extract property value based on property type + switch ($type) { + case 0x02: // 2 byte signed integer + $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset); + + break; + case 0x03: // 4 byte signed integer + $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset); + + break; + case 0x13: // 4 byte unsigned integer + // not needed yet, fix later if necessary + break; + case 0x1E: // null-terminated string prepended by dword string length + $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset); + $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength); + $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage); + $value = rtrim($value); + + break; + case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + // PHP-time + $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8)); + + break; + case 0x47: // Clipboard format + // not needed yet, fix later if necessary + break; + } + + switch ($id) { + case 0x01: // Code Page + $codePage = CodePage::numberToName((int) $value); + + break; + case 0x02: // Title + $this->spreadsheet->getProperties()->setTitle("$value"); + + break; + case 0x03: // Subject + $this->spreadsheet->getProperties()->setSubject("$value"); + + break; + case 0x04: // Author (Creator) + $this->spreadsheet->getProperties()->setCreator("$value"); + + break; + case 0x05: // Keywords + $this->spreadsheet->getProperties()->setKeywords("$value"); + + break; + case 0x06: // Comments (Description) + $this->spreadsheet->getProperties()->setDescription("$value"); + + break; + case 0x07: // Template + // Not supported by PhpSpreadsheet + break; + case 0x08: // Last Saved By (LastModifiedBy) + $this->spreadsheet->getProperties()->setLastModifiedBy("$value"); + + break; + case 0x09: // Revision + // Not supported by PhpSpreadsheet + break; + case 0x0A: // Total Editing Time + // Not supported by PhpSpreadsheet + break; + case 0x0B: // Last Printed + // Not supported by PhpSpreadsheet + break; + case 0x0C: // Created Date/Time + $this->spreadsheet->getProperties()->setCreated($value); + + break; + case 0x0D: // Modified Date/Time + $this->spreadsheet->getProperties()->setModified($value); + + break; + case 0x0E: // Number of Pages + // Not supported by PhpSpreadsheet + break; + case 0x0F: // Number of Words + // Not supported by PhpSpreadsheet + break; + case 0x10: // Number of Characters + // Not supported by PhpSpreadsheet + break; + case 0x11: // Thumbnail + // Not supported by PhpSpreadsheet + break; + case 0x12: // Name of creating application + // Not supported by PhpSpreadsheet + break; + case 0x13: // Security + // Not supported by PhpSpreadsheet + break; + } + } + } + + /** + * Read additional document summary information. + */ + protected function readDocumentSummaryInformation(): void + { + if (!isset($this->documentSummaryInformation)) { + return; + } + + // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark) + // offset: 2; size: 2; + // offset: 4; size: 2; OS version + // offset: 6; size: 2; OS indicator + // offset: 8; size: 16 + // offset: 24; size: 4; section count + //$secCount = self::getInt4d($this->documentSummaryInformation, 24); + + // offset: 28; size: 16; first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae + // offset: 44; size: 4; first section offset + $secOffset = self::getInt4d($this->documentSummaryInformation, 44); + + // section header + // offset: $secOffset; size: 4; section length + //$secLength = self::getInt4d($this->documentSummaryInformation, $secOffset); + + // offset: $secOffset+4; size: 4; property count + $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4); + + // initialize code page (used to resolve string values) + $codePage = 'CP1252'; + + // offset: ($secOffset+8); size: var + // loop through property decarations and properties + for ($i = 0; $i < $countProperties; ++$i) { + // offset: ($secOffset+8) + (8 * $i); size: 4; property ID + $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i)); + + // Use value of property id as appropriate + // offset: 60 + 8 * $i; size: 4; offset from beginning of section (48) + $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i)); + + $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset); + + // initialize property value + $value = null; + + // extract property value based on property type + switch ($type) { + case 0x02: // 2 byte signed integer + $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset); + + break; + case 0x03: // 4 byte signed integer + $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset); + + break; + case 0x0B: // Boolean + $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset); + $value = ($value == 0 ? false : true); + + break; + case 0x13: // 4 byte unsigned integer + // not needed yet, fix later if necessary + break; + case 0x1E: // null-terminated string prepended by dword string length + $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset); + $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength); + $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage); + $value = rtrim($value); + + break; + case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601) + // PHP-Time + $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8)); + + break; + case 0x47: // Clipboard format + // not needed yet, fix later if necessary + break; + } + + switch ($id) { + case 0x01: // Code Page + $codePage = CodePage::numberToName((int) $value); + + break; + case 0x02: // Category + $this->spreadsheet->getProperties()->setCategory("$value"); + + break; + case 0x03: // Presentation Target + // Not supported by PhpSpreadsheet + break; + case 0x04: // Bytes + // Not supported by PhpSpreadsheet + break; + case 0x05: // Lines + // Not supported by PhpSpreadsheet + break; + case 0x06: // Paragraphs + // Not supported by PhpSpreadsheet + break; + case 0x07: // Slides + // Not supported by PhpSpreadsheet + break; + case 0x08: // Notes + // Not supported by PhpSpreadsheet + break; + case 0x09: // Hidden Slides + // Not supported by PhpSpreadsheet + break; + case 0x0A: // MM Clips + // Not supported by PhpSpreadsheet + break; + case 0x0B: // Scale Crop + // Not supported by PhpSpreadsheet + break; + case 0x0C: // Heading Pairs + // Not supported by PhpSpreadsheet + break; + case 0x0D: // Titles of Parts + // Not supported by PhpSpreadsheet + break; + case 0x0E: // Manager + $this->spreadsheet->getProperties()->setManager("$value"); + + break; + case 0x0F: // Company + $this->spreadsheet->getProperties()->setCompany("$value"); + + break; + case 0x10: // Links up-to-date + // Not supported by PhpSpreadsheet + break; + } + } + } + + /** + * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record. + */ + protected function readDefault(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + + // move stream pointer to next record + $this->pos += 4 + $length; + } + + /** + * The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions, + * this record stores a note (cell note). This feature was significantly enhanced in Excel 97. + */ + protected function readNote(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($recordData, 0, 4)); + if ($this->version == self::XLS_BIFF8) { + $noteObjID = self::getUInt2d($recordData, 6); + $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8)); + $noteAuthor = $noteAuthor['value']; + $this->cellNotes[$noteObjID] = [ + 'cellRef' => $cellAddress, + 'objectID' => $noteObjID, + 'author' => $noteAuthor, + ]; + } else { + $extension = false; + if ($cellAddress == '$B$65536') { + // If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation + // note from the previous cell annotation. We're not yet handling this, so annotations longer than the + // max 2048 bytes will probably throw a wobbly. + //$row = self::getUInt2d($recordData, 0); + $extension = true; + $arrayKeys = array_keys($this->phpSheet->getComments()); + $cellAddress = array_pop($arrayKeys); + } + + $cellAddress = str_replace('$', '', (string) $cellAddress); + //$noteLength = self::getUInt2d($recordData, 4); + $noteText = trim(substr($recordData, 6)); + + if ($extension) { + // Concatenate this extension with the currently set comment for the cell + $comment = $this->phpSheet->getComment($cellAddress); + $commentText = $comment->getText()->getPlainText(); + $comment->setText($this->parseRichText($commentText . $noteText)); + } else { + // Set comment for the cell + $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText)); +// ->setAuthor($author) + } + } + } + + /** + * The TEXT Object record contains the text associated with a cell annotation. + */ + protected function readTextObject(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // recordData consists of an array of subrecords looking like this: + // grbit: 2 bytes; Option Flags + // rot: 2 bytes; rotation + // cchText: 2 bytes; length of the text (in the first continue record) + // cbRuns: 2 bytes; length of the formatting (in the second continue record) + // followed by the continuation records containing the actual text and formatting + $grbitOpts = self::getUInt2d($recordData, 0); + $rot = self::getUInt2d($recordData, 2); + //$cchText = self::getUInt2d($recordData, 10); + $cbRuns = self::getUInt2d($recordData, 12); + $text = $this->getSplicedRecordData(); + + /** @var int[] */ + $tempSplice = $text['spliceOffsets']; + /** @var int */ + $temp = $tempSplice[0]; + /** @var int */ + $temp1 = $tempSplice[1]; + $textByte = $temp1 - $temp - 1; + /** @var string */ + $textRecordData = $text['recordData']; + $textStr = substr($textRecordData, $temp + 1, $textByte); + // get 1 byte + $is16Bit = ord($textRecordData[0]); + // it is possible to use a compressed format, + // which omits the high bytes of all characters, if they are all zero + if (($is16Bit & 0x01) === 0) { + $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1'); + } else { + $textStr = $this->decodeCodepage($textStr); + } + + $this->textObjects[$this->textObjRef] = [ + 'text' => $textStr, + 'format' => substr($textRecordData, $tempSplice[1], $cbRuns), + 'alignment' => $grbitOpts, + 'rotation' => $rot, + ]; + } + + /** + * Read BOF. + */ + protected function readBof(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = substr($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 2; size: 2; type of the following data + $substreamType = self::getUInt2d($recordData, 2); + + switch ($substreamType) { + case self::XLS_WORKBOOKGLOBALS: + $version = self::getUInt2d($recordData, 0); + if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) { + throw new Exception('Cannot read this Excel file. Version is too old.'); + } + $this->version = $version; + + break; + case self::XLS_WORKSHEET: + // do not use this version information for anything + // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream + break; + default: + // substream, e.g. chart + // just skip the entire substream + do { + $code = self::getUInt2d($this->data, $this->pos); + $this->readDefault(); + } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize); + + break; + } + } + + public function setEncryptionPassword(string $encryptionPassword): self + { + $this->encryptionPassword = $encryptionPassword; + + return $this; + } + + /** + * FILEPASS. + * + * This record is part of the File Protection Block. It + * contains information about the read/write password of the + * file. All record contents following this record will be + * encrypted. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + * + * The decryption functions and objects used from here on in + * are based on the source of Spreadsheet-ParseExcel: + * https://metacpan.org/release/Spreadsheet-ParseExcel + */ + protected function readFilepass(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + + if ($length < 54) { + throw new Exception('Unexpected file pass record length'); + } + + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (substr($recordData, 0, 2) !== "\x01\x00" || substr($recordData, 4, 2) !== "\x01\x00") { + throw new Exception('Unsupported encryption algorithm'); + } + if (!$this->verifyPassword($this->encryptionPassword, substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) { + throw new Exception('Decryption password incorrect'); + } + + $this->encryption = self::MS_BIFF_CRYPTO_RC4; + + // Decryption required from the record after next onwards + $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2); + } + + /** + * Make an RC4 decryptor for the given block. + * + * @param int $block Block for which to create decrypto + * @param string $valContext MD5 context state + */ + private function makeKey(int $block, string $valContext): Xls\RC4 + { + $pwarray = str_repeat("\0", 64); + + for ($i = 0; $i < 5; ++$i) { + $pwarray[$i] = $valContext[$i]; + } + + $pwarray[5] = chr($block & 0xFF); + $pwarray[6] = chr(($block >> 8) & 0xFF); + $pwarray[7] = chr(($block >> 16) & 0xFF); + $pwarray[8] = chr(($block >> 24) & 0xFF); + + $pwarray[9] = "\x80"; + $pwarray[56] = "\x48"; + + $md5 = new Xls\MD5(); + $md5->add($pwarray); + + $s = $md5->getContext(); + + return new Xls\RC4($s); + } + + /** + * Verify RC4 file password. + * + * @param string $password Password to check + * @param string $docid Document id + * @param string $salt_data Salt data + * @param string $hashedsalt_data Hashed salt data + * @param string $valContext Set to the MD5 context of the value + * + * @return bool Success + */ + private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, string &$valContext): bool + { + $pwarray = str_repeat("\0", 64); + + $iMax = strlen($password); + for ($i = 0; $i < $iMax; ++$i) { + $o = ord(substr($password, $i, 1)); + $pwarray[2 * $i] = chr($o & 0xFF); + $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xFF); + } + $pwarray[2 * $i] = chr(0x80); + $pwarray[56] = chr(($i << 4) & 0xFF); + + $md5 = new Xls\MD5(); + $md5->add($pwarray); + + $mdContext1 = $md5->getContext(); + + $offset = 0; + $keyoffset = 0; + $tocopy = 5; + + $md5->reset(); + + while ($offset != 16) { + if ((64 - $offset) < 5) { + $tocopy = 64 - $offset; + } + for ($i = 0; $i <= $tocopy; ++$i) { + $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i]; + } + $offset += $tocopy; + + if ($offset == 64) { + $md5->add($pwarray); + $keyoffset = $tocopy; + $tocopy = 5 - $tocopy; + $offset = 0; + + continue; + } + + $keyoffset = 0; + $tocopy = 5; + for ($i = 0; $i < 16; ++$i) { + $pwarray[$offset + $i] = $docid[$i]; + } + $offset += 16; + } + + $pwarray[16] = "\x80"; + for ($i = 0; $i < 47; ++$i) { + $pwarray[17 + $i] = "\0"; + } + $pwarray[56] = "\x80"; + $pwarray[57] = "\x0a"; + + $md5->add($pwarray); + $valContext = $md5->getContext(); + + $key = $this->makeKey(0, $valContext); + + $salt = $key->RC4($salt_data); + $hashedsalt = $key->RC4($hashedsalt_data); + + $salt .= "\x80" . str_repeat("\0", 47); + $salt[56] = "\x80"; + + $md5->reset(); + $md5->add($salt); + $mdContext2 = $md5->getContext(); + + return $mdContext2 == $hashedsalt; + } + + /** + * CODEPAGE. + * + * This record stores the text encoding used to write byte + * strings, stored as MS Windows code page identifier. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readCodepage(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; code page identifier + $codepage = self::getUInt2d($recordData, 0); + + $this->codepage = CodePage::numberToName($codepage); + } + + /** + * DATEMODE. + * + * This record specifies the base date for displaying date + * values. All dates are stored as count of days past this + * base date. In BIFF2-BIFF4 this record is part of the + * Calculation Settings Block. In BIFF5-BIFF8 it is + * stored in the Workbook Globals Substream. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readDateMode(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; 0 = base 1900, 1 = base 1904 + Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + if (ord($recordData[0]) == 1) { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + $this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904); + } + } + + /** + * Read a FONT record. + */ + protected function readFont(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + $objFont = new Font(); + + // offset: 0; size: 2; height of the font (in twips = 1/20 of a point) + $size = self::getUInt2d($recordData, 0); + $objFont->setSize($size / 20); + + // offset: 2; size: 2; option flags + // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8) + // bit: 1; mask 0x0002; italic + $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1; + if ($isItalic) { + $objFont->setItalic(true); + } + + // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8) + // bit: 3; mask 0x0008; strikethrough + $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3; + if ($isStrike) { + $objFont->setStrikethrough(true); + } + + // offset: 4; size: 2; colour index + $colorIndex = self::getUInt2d($recordData, 4); + $objFont->colorIndex = $colorIndex; + + // offset: 6; size: 2; font weight + $weight = self::getUInt2d($recordData, 6); // regular=400 bold=700 + if ($weight >= 550) { + $objFont->setBold(true); + } + + // offset: 8; size: 2; escapement type + $escapement = self::getUInt2d($recordData, 8); + CellFont::escapement($objFont, $escapement); + + // offset: 10; size: 1; underline type + $underlineType = ord($recordData[10]); + CellFont::underline($objFont, $underlineType); + + // offset: 11; size: 1; font family + // offset: 12; size: 1; character set + // offset: 13; size: 1; not used + // offset: 14; size: var; font name + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringShort(substr($recordData, 14)); + } else { + $string = $this->readByteStringShort(substr($recordData, 14)); + } + /** @var string[] $string */ + $objFont->setName($string['value']); + + $this->objFonts[] = $objFont; + } + } + + /** + * FORMAT. + * + * This record contains information about a number format. + * All FORMAT records occur together in a sequential list. + * + * In BIFF2-BIFF4 other records referencing a FORMAT record + * contain a zero-based index into this list. From BIFF5 on + * the FORMAT record contains the index itself that will be + * used by other records. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readFormat(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + $indexCode = self::getUInt2d($recordData, 0); + + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringLong(substr($recordData, 2)); + } else { + // BIFF7 + $string = $this->readByteStringShort(substr($recordData, 2)); + } + + $formatString = $string['value']; + // Apache Open Office sets wrong case writing to xls - issue 2239 + if ($formatString === 'GENERAL') { + $formatString = NumberFormat::FORMAT_GENERAL; + } + $this->formats[$indexCode] = $formatString; + } + } + + /** + * XF - Extended Format. + * + * This record contains formatting information for cells, rows, columns or styles. + * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF + * and 1 cell XF. + * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF + * and XF record 15 is a cell XF + * We only read the first cell style XF and skip the remaining cell style XF records + * We read all cell XF records. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readXf(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + $objStyle = new Style(); + + if (!$this->readDataOnly) { + // offset: 0; size: 2; Index to FONT record + if (self::getUInt2d($recordData, 0) < 4) { + $fontIndex = self::getUInt2d($recordData, 0); + } else { + // this has to do with that index 4 is omitted in all BIFF versions for some strange reason + // check the OpenOffice documentation of the FONT record + $fontIndex = self::getUInt2d($recordData, 0) - 1; + } + if (isset($this->objFonts[$fontIndex])) { + $objStyle->setFont($this->objFonts[$fontIndex]); + } + + // offset: 2; size: 2; Index to FORMAT record + $numberFormatIndex = self::getUInt2d($recordData, 2); + if (isset($this->formats[$numberFormatIndex])) { + // then we have user-defined format code + $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]]; + } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') { + // then we have built-in format code + $numberFormat = ['formatCode' => $code]; + } else { + // we set the general format code + $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL]; + } + /** @var string[] $numberFormat */ + $objStyle->getNumberFormat() + ->setFormatCode($numberFormat['formatCode']); + + // offset: 4; size: 2; XF type, cell protection, and parent style XF + // bit 2-0; mask 0x0007; XF_TYPE_PROT + $xfTypeProt = self::getUInt2d($recordData, 4); + // bit 0; mask 0x01; 1 = cell is locked + $isLocked = (0x01 & $xfTypeProt) >> 0; + $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED); + + // bit 1; mask 0x02; 1 = Formula is hidden + $isHidden = (0x02 & $xfTypeProt) >> 1; + $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED); + + // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF + $isCellStyleXf = (0x04 & $xfTypeProt) >> 2; + + // offset: 6; size: 1; Alignment and text break + // bit 2-0, mask 0x07; horizontal alignment + $horAlign = (0x07 & ord($recordData[6])) >> 0; + Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign); + + // bit 3, mask 0x08; wrap text + $wrapText = (0x08 & ord($recordData[6])) >> 3; + Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText); + + // bit 6-4, mask 0x70; vertical alignment + $vertAlign = (0x70 & ord($recordData[6])) >> 4; + Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign); + + if ($this->version == self::XLS_BIFF8) { + // offset: 7; size: 1; XF_ROTATION: Text rotation angle + $angle = ord($recordData[7]); + $rotation = 0; + if ($angle <= 90) { + $rotation = $angle; + } elseif ($angle <= 180) { + $rotation = 90 - $angle; + } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) { + $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET; + } + $objStyle->getAlignment()->setTextRotation($rotation); + + // offset: 8; size: 1; Indentation, shrink to cell size, and text direction + // bit: 3-0; mask: 0x0F; indent level + $indent = (0x0F & ord($recordData[8])) >> 0; + $objStyle->getAlignment()->setIndent($indent); + + // bit: 4; mask: 0x10; 1 = shrink content to fit into cell + $shrinkToFit = (0x10 & ord($recordData[8])) >> 4; + switch ($shrinkToFit) { + case 0: + $objStyle->getAlignment()->setShrinkToFit(false); + + break; + case 1: + $objStyle->getAlignment()->setShrinkToFit(true); + + break; + } + $readOrder = (0xC0 & ord($recordData[8])) >> 6; + $objStyle->getAlignment()->setReadOrder($readOrder); + + // offset: 9; size: 1; Flags used for attribute groups + + // offset: 10; size: 4; Cell border lines and background area + // bit: 3-0; mask: 0x0000000F; left style + if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) { + $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle); + } + // bit: 7-4; mask: 0x000000F0; right style + if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) { + $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle); + } + // bit: 11-8; mask: 0x00000F00; top style + if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) { + $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle); + } + // bit: 15-12; mask: 0x0000F000; bottom style + if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) { + $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle); + } + // bit: 22-16; mask: 0x007F0000; left color + $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16; + + // bit: 29-23; mask: 0x3F800000; right color + $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23; + + // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom + $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false; + + // bit: 31; mask: 0x800000; 1 = diagonal line from bottom left to top right + $diagonalUp = (self::HIGH_ORDER_BIT & self::getInt4d($recordData, 10)) >> 31 ? true : false; + + if ($diagonalUp === false) { + if ($diagonalDown === false) { + $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE); + } else { + $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN); + } + } elseif ($diagonalDown === false) { + $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP); + } else { + $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH); + } + + // offset: 14; size: 4; + // bit: 6-0; mask: 0x0000007F; top color + $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0; + + // bit: 13-7; mask: 0x00003F80; bottom color + $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7; + + // bit: 20-14; mask: 0x001FC000; diagonal color + $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14; + + // bit: 24-21; mask: 0x01E00000; diagonal style + if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) { + $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle); + } + + // bit: 31-26; mask: 0xFC000000 fill pattern + if ($fillType = FillPattern::lookup((self::FC000000 & self::getInt4d($recordData, 14)) >> 26)) { + $objStyle->getFill()->setFillType($fillType); + } + // offset: 18; size: 2; pattern and background colour + // bit: 6-0; mask: 0x007F; color index for pattern color + $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0; + + // bit: 13-7; mask: 0x3F80; color index for pattern background + $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7; + } else { + // BIFF5 + + // offset: 7; size: 1; Text orientation and flags + $orientationAndFlags = ord($recordData[7]); + + // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation + $xfOrientation = (0x03 & $orientationAndFlags) >> 0; + switch ($xfOrientation) { + case 0: + $objStyle->getAlignment()->setTextRotation(0); + + break; + case 1: + $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET); + + break; + case 2: + $objStyle->getAlignment()->setTextRotation(90); + + break; + case 3: + $objStyle->getAlignment()->setTextRotation(-90); + + break; + } + + // offset: 8; size: 4; cell border lines and background area + $borderAndBackground = self::getInt4d($recordData, 8); + + // bit: 6-0; mask: 0x0000007F; color index for pattern color + $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0; + + // bit: 13-7; mask: 0x00003F80; color index for pattern background + $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7; + + // bit: 21-16; mask: 0x003F0000; fill pattern + $objStyle->getFill()->setFillType(FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16)); + + // bit: 24-22; mask: 0x01C00000; bottom line style + $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22)); + + // bit: 31-25; mask: 0xFE000000; bottom line color + $objStyle->getBorders()->getBottom()->colorIndex = (self::FE000000 & $borderAndBackground) >> 25; + + // offset: 12; size: 4; cell border lines + $borderLines = self::getInt4d($recordData, 12); + + // bit: 2-0; mask: 0x00000007; top line style + $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0)); + + // bit: 5-3; mask: 0x00000038; left line style + $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3)); + + // bit: 8-6; mask: 0x000001C0; right line style + $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6)); + + // bit: 15-9; mask: 0x0000FE00; top line color index + $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9; + + // bit: 22-16; mask: 0x007F0000; left line color index + $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16; + + // bit: 29-23; mask: 0x3F800000; right line color index + $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23; + } + + // add cellStyleXf or cellXf and update mapping + if ($isCellStyleXf) { + // we only read one style XF record which is always the first + if ($this->xfIndex == 0) { + $this->spreadsheet->addCellStyleXf($objStyle); + $this->mapCellStyleXfIndex[$this->xfIndex] = 0; + } + } else { + // we read all cell XF records + $this->spreadsheet->addCellXf($objStyle); + $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1; + } + + // update XF index for when we read next record + ++$this->xfIndex; + } + } + + protected function readXfExt(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; 0x087D = repeated header + + // offset: 2; size: 2 + + // offset: 4; size: 8; not used + + // offset: 12; size: 2; record version + + // offset: 14; size: 2; index to XF record which this record modifies + $ixfe = self::getUInt2d($recordData, 14); + + // offset: 16; size: 2; not used + + // offset: 18; size: 2; number of extension properties that follow + //$cexts = self::getUInt2d($recordData, 18); + + // start reading the actual extension data + $offset = 20; + while ($offset < $length) { + // extension type + $extType = self::getUInt2d($recordData, $offset); + + // extension length + $cb = self::getUInt2d($recordData, $offset + 2); + + // extension data + $extData = substr($recordData, $offset + 4, $cb); + + switch ($extType) { + case 4: // fill start color + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill(); + $fill->getStartColor()->setRGB($rgb); + $fill->startcolorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 5: // fill end color + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill(); + $fill->getEndColor()->setRGB($rgb); + $fill->endcolorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 7: // border color top + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop(); + $top->getColor()->setRGB($rgb); + $top->colorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 8: // border color bottom + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom(); + $bottom->getColor()->setRGB($rgb); + $bottom->colorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 9: // border color left + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft(); + $left->getColor()->setRGB($rgb); + $left->colorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 10: // border color right + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight(); + $right->getColor()->setRGB($rgb); + $right->colorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 11: // border color diagonal + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal(); + $diagonal->getColor()->setRGB($rgb); + $diagonal->colorIndex = null; // normal color index does not apply, discard + } + } + + break; + case 13: // font color + $xclfType = self::getUInt2d($extData, 0); // color type + $xclrValue = substr($extData, 4, 4); // color value (value based on color type) + + if ($xclfType == 2) { + $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2])); + + // modify the relevant style property + if (isset($this->mapCellXfIndex[$ixfe])) { + $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont(); + $font->getColor()->setRGB($rgb); + $font->colorIndex = null; // normal color index does not apply, discard + } + } + + break; + } + + $offset += $cb; + } + } + } + + /** + * Read STYLE record. + */ + protected function readStyle(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; index to XF record and flag for built-in style + $ixfe = self::getUInt2d($recordData, 0); + + // bit: 11-0; mask 0x0FFF; index to XF record + //$xfIndex = (0x0FFF & $ixfe) >> 0; + + // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style + $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15); + + if ($isBuiltIn) { + // offset: 2; size: 1; identifier for built-in style + $builtInId = ord($recordData[2]); + + switch ($builtInId) { + case 0x00: + // currently, we are not using this for anything + break; + default: + break; + } + } + // user-defined; not supported by PhpSpreadsheet + } + } + + /** + * Read PALETTE record. + */ + protected function readPalette(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; number of following colors + $nm = self::getUInt2d($recordData, 0); + + // list of RGB colors + for ($i = 0; $i < $nm; ++$i) { + $rgb = substr($recordData, 2 + 4 * $i, 4); + $this->palette[] = self::readRGB($rgb); + } + } + } + + /** + * SHEET. + * + * This record is located in the Workbook Globals + * Substream and represents a sheet inside the workbook. + * One SHEET record is written for each sheet. It stores the + * sheet name and a stream offset to the BOF record of the + * respective Sheet Substream within the Workbook Stream. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readSheet(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // offset: 0; size: 4; absolute stream position of the BOF record of the sheet + // NOTE: not encrypted + $rec_offset = self::getInt4d($this->data, $this->pos + 4); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 4; size: 1; sheet state + $sheetState = match (ord($recordData[4])) { + 0x00 => Worksheet::SHEETSTATE_VISIBLE, + 0x01 => Worksheet::SHEETSTATE_HIDDEN, + 0x02 => Worksheet::SHEETSTATE_VERYHIDDEN, + default => Worksheet::SHEETSTATE_VISIBLE, + }; + + // offset: 5; size: 1; sheet type + $sheetType = ord($recordData[5]); + + // offset: 6; size: var; sheet name + $rec_name = null; + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringShort(substr($recordData, 6)); + $rec_name = $string['value']; + } elseif ($this->version == self::XLS_BIFF7) { + $string = $this->readByteStringShort(substr($recordData, 6)); + $rec_name = $string['value']; + } + /** @var string $rec_name */ + $this->sheets[] = [ + 'name' => $rec_name, + 'offset' => $rec_offset, + 'sheetState' => $sheetState, + 'sheetType' => $sheetType, + ]; + } + + /** + * Read EXTERNALBOOK record. + */ + protected function readExternalBook(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset within record data + $offset = 0; + + // there are 4 types of records + if (strlen($recordData) > 4) { + // external reference + // offset: 0; size: 2; number of sheet names ($nm) + $nm = self::getUInt2d($recordData, 0); + $offset += 2; + + // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length) + $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2)); + $offset += $encodedUrlString['size']; + + // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length) + $externalSheetNames = []; + for ($i = 0; $i < $nm; ++$i) { + $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset)); + $externalSheetNames[] = $externalSheetNameString['value']; + $offset += $externalSheetNameString['size']; + } + + // store the record data + $this->externalBooks[] = [ + 'type' => 'external', + 'encodedUrl' => $encodedUrlString['value'], + 'externalSheetNames' => $externalSheetNames, + ]; + } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) { + // internal reference + // offset: 0; size: 2; number of sheet in this document + // offset: 2; size: 2; 0x01 0x04 + $this->externalBooks[] = [ + 'type' => 'internal', + ]; + } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) { + // add-in function + // offset: 0; size: 2; 0x0001 + $this->externalBooks[] = [ + 'type' => 'addInFunction', + ]; + } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) { + // DDE links, OLE links + // offset: 0; size: 2; 0x0000 + // offset: 2; size: var; encoded source document name + $this->externalBooks[] = [ + 'type' => 'DDEorOLE', + ]; + } + } + + /** + * Read EXTERNNAME record. + */ + protected function readExternName(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // external sheet references provided for named cells + if ($this->version == self::XLS_BIFF8) { + // offset: 0; size: 2; options + //$options = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; + + // offset: 4; size: 2; not used + + // offset: 6; size: var + $nameString = self::readUnicodeStringShort(substr($recordData, 6)); + + // offset: var; size: var; formula data + $offset = 6 + $nameString['size']; + $formula = $this->getFormulaFromStructure(substr($recordData, $offset)); + + $this->externalNames[] = [ + 'name' => $nameString['value'], + 'formula' => $formula, + ]; + } + } + + /** + * Read EXTERNSHEET record. + */ + protected function readExternSheet(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // external sheet references provided for named cells + if ($this->version == self::XLS_BIFF8) { + // offset: 0; size: 2; number of following ref structures + $nm = self::getUInt2d($recordData, 0); + for ($i = 0; $i < $nm; ++$i) { + $this->ref[] = [ + // offset: 2 + 6 * $i; index to EXTERNALBOOK record + 'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i), + // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record + 'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i), + // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record + 'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i), + ]; + } + } + } + + /** + * DEFINEDNAME. + * + * This record is part of a Link Table. It contains the name + * and the token array of an internal defined name. Token + * arrays of defined names contain tokens with aberrant + * token classes. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readDefinedName(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->version == self::XLS_BIFF8) { + // retrieves named cells + + // offset: 0; size: 2; option flags + $opts = self::getUInt2d($recordData, 0); + + // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name + $isBuiltInName = (0x0020 & $opts) >> 5; + + // offset: 2; size: 1; keyboard shortcut + + // offset: 3; size: 1; length of the name (character count) + $nlen = ord($recordData[3]); + + // offset: 4; size: 2; size of the formula data (it can happen that this is zero) + // note: there can also be additional data, this is not included in $flen + $flen = self::getUInt2d($recordData, 4); + + // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based) + $scope = self::getUInt2d($recordData, 8); + + // offset: 14; size: var; Name (Unicode string without length field) + $string = self::readUnicodeString(substr($recordData, 14), $nlen); + + // offset: var; size: $flen; formula data + $offset = 14 + $string['size']; + $formulaStructure = pack('v', $flen) . substr($recordData, $offset); + + try { + $formula = $this->getFormulaFromStructure($formulaStructure); + } catch (PhpSpreadsheetException) { + $formula = ''; + $isBuiltInName = 0; + } + + $this->definedname[] = [ + 'isBuiltInName' => $isBuiltInName, + 'name' => $string['value'], + 'formula' => $formula, + 'scope' => $scope, + ]; + } + } + + /** + * Read MSODRAWINGGROUP record. + */ + protected function readMsoDrawingGroup(): void + { + //$length = self::getUInt2d($this->data, $this->pos + 2); + + // get spliced record data + $splicedRecordData = $this->getSplicedRecordData(); + /** @var string */ + $recordData = $splicedRecordData['recordData']; + + $this->drawingGroupData .= $recordData; + } + + /** + * SST - Shared String Table. + * + * This record contains a list of all strings used anywhere + * in the workbook. Each string occurs only once. The + * workbook uses indexes into the list to reference the + * strings. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readSst(): void + { + // offset within (spliced) record data + $pos = 0; + + // Limit global SST position, further control for bad SST Length in BIFF8 data + $limitposSST = 0; + + // get spliced record data + $splicedRecordData = $this->getSplicedRecordData(); + + $recordData = $splicedRecordData['recordData']; + /** @var mixed[] */ + $spliceOffsets = $splicedRecordData['spliceOffsets']; + + // offset: 0; size: 4; total number of strings in the workbook + $pos += 4; + + // offset: 4; size: 4; number of following strings ($nm) + /** @var string $recordData */ + $nm = self::getInt4d($recordData, 4); + $pos += 4; + + // look up limit position + foreach ($spliceOffsets as $spliceOffset) { + // it can happen that the string is empty, therefore we need + // <= and not just < + if ($pos <= $spliceOffset) { + $limitposSST = $spliceOffset; + } + } + + // loop through the Unicode strings (16-bit length) + for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) { + // number of characters in the Unicode string + /** @var int $pos */ + $numChars = self::getUInt2d($recordData, $pos); + /** @var int $pos */ + $pos += 2; + + // option flags + /** @var string $recordData */ + $optionFlags = ord($recordData[$pos]); + ++$pos; + + // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed + $isCompressed = (($optionFlags & 0x01) == 0); + + // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic + $hasAsian = (($optionFlags & 0x04) != 0); + + // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text + $hasRichText = (($optionFlags & 0x08) != 0); + + $formattingRuns = 0; + if ($hasRichText) { + // number of Rich-Text formatting runs + $formattingRuns = self::getUInt2d($recordData, $pos); + $pos += 2; + } + + $extendedRunLength = 0; + if ($hasAsian) { + // size of Asian phonetic setting + $extendedRunLength = self::getInt4d($recordData, $pos); + $pos += 4; + } + + // expected byte length of character array if not split + $len = ($isCompressed) ? $numChars : $numChars * 2; + + // look up limit position - Check it again to be sure that no error occurs when parsing SST structure + $limitpos = null; + foreach ($spliceOffsets as $spliceOffset) { + // it can happen that the string is empty, therefore we need + // <= and not just < + if ($pos <= $spliceOffset) { + $limitpos = $spliceOffset; + + break; + } + } + + /** @var int $limitpos */ + if ($pos + $len <= $limitpos) { + // character array is not split between records + + $retstr = substr($recordData, $pos, $len); + $pos += $len; + } else { + // character array is split between records + + // first part of character array + $retstr = substr($recordData, $pos, $limitpos - $pos); + + $bytesRead = $limitpos - $pos; + + // remaining characters in Unicode string + $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2)); + + $pos = $limitpos; + + // keep reading the characters + while ($charsLeft > 0) { + // look up next limit position, in case the string span more than one continue record + foreach ($spliceOffsets as $spliceOffset) { + if ($pos < $spliceOffset) { + $limitpos = $spliceOffset; + + break; + } + } + + // repeated option flags + // OpenOffice.org documentation 5.21 + /** @var int $pos */ + $option = ord($recordData[$pos]); + ++$pos; + + /** @var int $limitpos */ + if ($isCompressed && ($option == 0)) { + // 1st fragment compressed + // this fragment compressed + /** @var int */ + $len = min($charsLeft, $limitpos - $pos); + $retstr .= substr($recordData, $pos, $len); + $charsLeft -= $len; + $isCompressed = true; + } elseif (!$isCompressed && ($option != 0)) { + // 1st fragment uncompressed + // this fragment uncompressed + /** @var int */ + $len = min($charsLeft * 2, $limitpos - $pos); + $retstr .= substr($recordData, $pos, $len); + $charsLeft -= $len / 2; + $isCompressed = false; + } elseif (!$isCompressed && ($option == 0)) { + // 1st fragment uncompressed + // this fragment compressed + $len = min($charsLeft, $limitpos - $pos); + for ($j = 0; $j < $len; ++$j) { + $retstr .= $recordData[$pos + $j] + . chr(0); + } + $charsLeft -= $len; + $isCompressed = false; + } else { + // 1st fragment compressed + // this fragment uncompressed + $newstr = ''; + $jMax = strlen($retstr); + for ($j = 0; $j < $jMax; ++$j) { + $newstr .= $retstr[$j] . chr(0); + } + $retstr = $newstr; + /** @var int */ + $len = min($charsLeft * 2, $limitpos - $pos); + $retstr .= substr($recordData, $pos, $len); + $charsLeft -= $len / 2; + $isCompressed = false; + } + + $pos += $len; + } + } + + // convert to UTF-8 + $retstr = self::encodeUTF16($retstr, $isCompressed); + + // read additional Rich-Text information, if any + $fmtRuns = []; + if ($hasRichText) { + // list of formatting runs + for ($j = 0; $j < $formattingRuns; ++$j) { + // first formatted character; zero-based + /** @var int $pos */ + $charPos = self::getUInt2d($recordData, $pos + $j * 4); + + // index to font record + $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4); + + $fmtRuns[] = [ + 'charPos' => $charPos, + 'fontIndex' => $fontIndex, + ]; + } + $pos += 4 * $formattingRuns; + } + + // read additional Asian phonetics information, if any + if ($hasAsian) { + // For Asian phonetic settings, we skip the extended string data + $pos += $extendedRunLength; + } + + // store the shared sting + $this->sst[] = [ + 'value' => $retstr, + 'fmtRuns' => $fmtRuns, + ]; + } + + // getSplicedRecordData() takes care of moving current position in data stream + } + + /** + * Read PRINTGRIDLINES record. + */ + protected function readPrintGridlines(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) { + // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines + $printGridlines = (bool) self::getUInt2d($recordData, 0); + $this->phpSheet->setPrintGridlines($printGridlines); + } + } + + /** + * Read DEFAULTROWHEIGHT record. + */ + protected function readDefaultRowHeight(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; option flags + // offset: 2; size: 2; default height for unused rows, (twips 1/20 point) + $height = self::getUInt2d($recordData, 2); + $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20); + } + + /** + * Read SHEETPR record. + */ + protected function readSheetPr(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2 + + // bit: 6; mask: 0x0040; 0 = outline buttons above outline group + $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6; + $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow); + + // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group + $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7; + $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight); + + // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages + // this corresponds to radio button setting in page setup dialog in Excel + $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8); + } + + /** + * Read HORIZONTALPAGEBREAKS record. + */ + protected function readHorizontalPageBreaks(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) { + // offset: 0; size: 2; number of the following row index structures + $nm = self::getUInt2d($recordData, 0); + + // offset: 2; size: 6 * $nm; list of $nm row index structures + for ($i = 0; $i < $nm; ++$i) { + $r = self::getUInt2d($recordData, 2 + 6 * $i); + $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2); + //$cl = self::getUInt2d($recordData, 2 + 6 * $i + 4); + + // not sure why two column indexes are necessary? + $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW); + } + } + } + + /** + * Read VERTICALPAGEBREAKS record. + */ + protected function readVerticalPageBreaks(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) { + // offset: 0; size: 2; number of the following column index structures + $nm = self::getUInt2d($recordData, 0); + + // offset: 2; size: 6 * $nm; list of $nm row index structures + for ($i = 0; $i < $nm; ++$i) { + $c = self::getUInt2d($recordData, 2 + 6 * $i); + $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2); + //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4); + + // not sure why two row indexes are necessary? + $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN); + } + } + } + + /** + * Read HEADER record. + */ + protected function readHeader(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: var + // realized that $recordData can be empty even when record exists + if ($recordData) { + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringLong($recordData); + } else { + $string = $this->readByteStringShort($recordData); + } + + /** @var string[] $string */ + $this->phpSheet + ->getHeaderFooter() + ->setOddHeader($string['value']); + $this->phpSheet + ->getHeaderFooter() + ->setEvenHeader($string['value']); + } + } + } + + /** + * Read FOOTER record. + */ + protected function readFooter(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: var + // realized that $recordData can be empty even when record exists + if ($recordData) { + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringLong($recordData); + } else { + $string = $this->readByteStringShort($recordData); + } + /** @var string */ + $temp = $string['value']; + $this->phpSheet + ->getHeaderFooter() + ->setOddFooter($temp); + $this->phpSheet + ->getHeaderFooter() + ->setEvenFooter($temp); + } + } + } + + /** + * Read HCENTER record. + */ + protected function readHcenter(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally + $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0); + + $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered); + } + } + + /** + * Read VCENTER record. + */ + protected function readVcenter(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered + $isVerticalCentered = (bool) self::getUInt2d($recordData, 0); + + $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered); + } + } + + /** + * Read LEFTMARGIN record. + */ + protected function readLeftMargin(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 8 + $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData)); + } + } + + /** + * Read RIGHTMARGIN record. + */ + protected function readRightMargin(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 8 + $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData)); + } + } + + /** + * Read TOPMARGIN record. + */ + protected function readTopMargin(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 8 + $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData)); + } + } + + /** + * Read BOTTOMMARGIN record. + */ + protected function readBottomMargin(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 8 + $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData)); + } + } + + /** + * Read PAGESETUP record. + */ + protected function readPageSetup(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; paper size + $paperSize = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; scaling factor + $scale = self::getUInt2d($recordData, 2); + + // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed + $fitToWidth = self::getUInt2d($recordData, 6); + + // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed + $fitToHeight = self::getUInt2d($recordData, 8); + + // offset: 10; size: 2; option flags + + // bit: 0; mask: 0x0001; 0=down then over, 1=over then down + $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10)); + + // bit: 1; mask: 0x0002; 0=landscape, 1=portrait + $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1; + + // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init + // when this bit is set, do not use flags for those properties + $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2; + + if (!$isNotInit) { + $this->phpSheet->getPageSetup()->setPaperSize($paperSize); + $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER); + $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE); + + $this->phpSheet->getPageSetup()->setScale($scale, false); + $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages); + $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false); + $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false); + } + + // offset: 16; size: 8; header margin (IEEE 754 floating-point value) + $marginHeader = self::extractNumber(substr($recordData, 16, 8)); + $this->phpSheet->getPageMargins()->setHeader($marginHeader); + + // offset: 24; size: 8; footer margin (IEEE 754 floating-point value) + $marginFooter = self::extractNumber(substr($recordData, 24, 8)); + $this->phpSheet->getPageMargins()->setFooter($marginFooter); + } + } + + /** + * PROTECT - Sheet protection (BIFF2 through BIFF8) + * if this record is omitted, then it also means no sheet protection. + */ + protected function readProtect(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; + + // bit 0, mask 0x01; 1 = sheet is protected + $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0; + $this->phpSheet->getProtection()->setSheet((bool) $bool); + } + + /** + * SCENPROTECT. + */ + protected function readScenProtect(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; + + // bit: 0, mask 0x01; 1 = scenarios are protected + $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0; + + $this->phpSheet->getProtection()->setScenarios((bool) $bool); + } + + /** + * OBJECTPROTECT. + */ + protected function readObjectProtect(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; + + // bit: 0, mask 0x01; 1 = objects are protected + $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0; + + $this->phpSheet->getProtection()->setObjects((bool) $bool); + } + + /** + * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8). + */ + protected function readPassword(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; 16-bit hash value of password + $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password + $this->phpSheet->getProtection()->setPassword($password, true); + } + } + + /** + * Read DEFCOLWIDTH record. + */ + protected function readDefColWidth(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; default column width + $width = self::getUInt2d($recordData, 0); + if ($width != 8) { + $this->phpSheet->getDefaultColumnDimension()->setWidth($width); + } + } + + /** + * Read COLINFO record. + */ + protected function readColInfo(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; index to first column in range + $firstColumnIndex = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to last column in range + $lastColumnIndex = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character + $width = self::getUInt2d($recordData, 4); + + // offset: 6; size: 2; index to XF record for default column formatting + $xfIndex = self::getUInt2d($recordData, 6); + + // offset: 8; size: 2; option flags + // bit: 0; mask: 0x0001; 1= columns are hidden + $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0; + + // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline) + $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8; + + // bit: 12; mask: 0x1000; 1 = collapsed + $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12); + + // offset: 10; size: 2; not used + + for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) { + if ($lastColumnIndex == 255 || $lastColumnIndex == 256) { + $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256); + + break; + } + $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256); + $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden); + $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level); + $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed); + if (isset($this->mapCellXfIndex[$xfIndex])) { + $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + } + + /** + * ROW. + * + * This record contains the properties of a single row in a + * sheet. Rows and cells in a sheet are divided into blocks + * of 32 rows. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readRow(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; index of this row + $r = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to column of the first cell which is described by a cell record + + // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1 + + // offset: 6; size: 2; + + // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point + $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0; + + // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height + $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15; + + if (!$useDefaultHeight) { + if ( + $this->phpSheet->getDefaultRowDimension()->getRowHeight() > 0 + ) { + $this->phpSheet->getRowDimension($r + 1) + ->setCustomFormat(true, ($height === 255) ? -1 : ($height / 20)); + } else { + $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20); + } + } + + // offset: 8; size: 2; not used + + // offset: 10; size: 2; not used in BIFF5-BIFF8 + + // offset: 12; size: 4; option flags and default row formatting + + // bit: 2-0: mask: 0x00000007; outline level of the row + $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0; + $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level); + + // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed + $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4); + $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed); + + // bit: 5; mask: 0x00000020; 1 = row is hidden + $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5; + $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden); + + // bit: 7; mask: 0x00000080; 1 = row has explicit format + $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7; + + // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record + $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16; + + if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) { + $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + + /** + * Read RK record + * This record represents a cell that contains an RK value + * (encoded integer or floating-point value). If a + * floating-point value cannot be encoded to an RK value, + * a NUMBER record will be written. This record replaces the + * record INTEGER written in BIFF2. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readRk(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; index to row + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to column + $column = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($column + 1); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset: 4; size: 2; index to XF record + $xfIndex = self::getUInt2d($recordData, 4); + + // offset: 6; size: 4; RK value + $rknum = self::getInt4d($recordData, 6); + $numValue = self::getIEEE754($rknum); + + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { + // add style information + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + + // add cell + $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC); + } + } + + /** + * Read LABELSST record + * This record represents a cell that contains a string. It + * replaces the LABEL record and RSTRING record used in + * BIFF2-BIFF5. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readLabelSst(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; index to row + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to column + $column = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($column + 1); + + $cell = null; + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset: 4; size: 2; index to XF record + $xfIndex = self::getUInt2d($recordData, 4); + + // offset: 6; size: 4; index to SST record + $index = self::getInt4d($recordData, 6); + + // add cell + if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) { + // then we should treat as rich text + $richText = new RichText(); + $charPos = 0; + $sstCount = count($this->sst[$index]['fmtRuns']); + for ($i = 0; $i <= $sstCount; ++$i) { + /** @var mixed[][] $fmtRuns */ + if (isset($fmtRuns[$i])) { + /** @var int[] */ + $temp = $fmtRuns[$i]; + $temp = $temp['charPos']; + /** @var int $charPos */ + $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $temp - $charPos); + $charPos = $temp; + } else { + $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value'])); + } + + if (StringHelper::countCharacters($text) > 0) { + if ($i == 0) { // first text run, no style + $richText->createText($text); + } else { + $textRun = $richText->createTextRun($text); + /** @var int[][] $fmtRuns */ + if (isset($fmtRuns[$i - 1])) { + if ($fmtRuns[$i - 1]['fontIndex'] < 4) { + $fontIndex = $fmtRuns[$i - 1]['fontIndex']; + } else { + // this has to do with that index 4 is omitted in all BIFF versions for some stra nge reason + // check the OpenOffice documentation of the FONT record + /** @var int */ + $temp = $fmtRuns[$i - 1]['fontIndex']; + $fontIndex = $temp - 1; + } + if (array_key_exists($fontIndex, $this->objFonts) === false) { + $fontIndex = count($this->objFonts) - 1; + } + $textRun->setFont(clone $this->objFonts[$fontIndex]); + } + } + } + } + if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') { + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + $cell->setValueExplicit($richText, DataType::TYPE_STRING); + } + } else { + if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') { + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING); + } + } + + if (!$this->readDataOnly && $cell !== null && isset($this->mapCellXfIndex[$xfIndex])) { + // add style information + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + + /** + * Read MULRK record + * This record represents a cell range containing RK value + * cells. All cells are located in the same row. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readMulRk(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; index to row + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to first column + $colFirst = self::getUInt2d($recordData, 2); + + // offset: var; size: 2; index to last column + $colLast = self::getUInt2d($recordData, $length - 2); + $columns = $colLast - $colFirst + 1; + + // offset within record data + $offset = 4; + + for ($i = 1; $i <= $columns; ++$i) { + $columnString = Coordinate::stringFromColumnIndex($colFirst + $i); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset: var; size: 2; index to XF record + $xfIndex = self::getUInt2d($recordData, $offset); + + // offset: var; size: 4; RK value + $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2)); + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { + // add style + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + + // add cell value + $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC); + } + + $offset += 6; + } + } + + /** + * Read NUMBER record + * This record represents a cell that contains a + * floating-point value. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readNumber(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; index to row + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size 2; index to column + $column = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($column + 1); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset 4; size: 2; index to XF record + $xfIndex = self::getUInt2d($recordData, 4); + + $numValue = self::extractNumber(substr($recordData, 6, 8)); + + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { + // add cell style + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + + // add cell value + $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC); + } + } + + /** + * Read FORMULA record + perhaps a following STRING record if formula result is a string + * This record contains the token array and the result of a + * formula cell. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readFormula(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; row index + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; col index + $column = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($column + 1); + + // offset: 20: size: variable; formula structure + $formulaStructure = substr($recordData, 20); + + // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc. + $options = self::getUInt2d($recordData, 14); + + // bit: 0; mask: 0x0001; 1 = recalculate always + // bit: 1; mask: 0x0002; 1 = calculate on open + // bit: 2; mask: 0x0008; 1 = part of a shared formula + $isPartOfSharedFormula = (bool) (0x0008 & $options); + + // WARNING: + // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true + // the formula data may be ordinary formula data, therefore we need to check + // explicitly for the tExp token (0x01) + $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01; + + if ($isPartOfSharedFormula) { + // part of shared formula which means there will be a formula with a tExp token and nothing else + // get the base cell, grab tExp token + $baseRow = self::getUInt2d($formulaStructure, 3); + $baseCol = self::getUInt2d($formulaStructure, 5); + $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1); + } + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($isPartOfSharedFormula) { + // formula is added to this cell after the sheet has been read + $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell; + } + + // offset: 16: size: 4; not used + + // offset: 4; size: 2; XF index + $xfIndex = self::getUInt2d($recordData, 4); + + // offset: 6; size: 8; result of the formula + if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) { + // String formula. Result follows in appended STRING record + $dataType = DataType::TYPE_STRING; + + // read possible SHAREDFMLA record + $code = self::getUInt2d($this->data, $this->pos); + if ($code == self::XLS_TYPE_SHAREDFMLA) { + $this->readSharedFmla(); + } + + // read STRING record + $value = $this->readString(); + } elseif ( + (ord($recordData[6]) == 1) + && (ord($recordData[12]) == 255) + && (ord($recordData[13]) == 255) + ) { + // Boolean formula. Result is in +2; 0=false, 1=true + $dataType = DataType::TYPE_BOOL; + $value = (bool) ord($recordData[8]); + } elseif ( + (ord($recordData[6]) == 2) + && (ord($recordData[12]) == 255) + && (ord($recordData[13]) == 255) + ) { + // Error formula. Error code is in +2 + $dataType = DataType::TYPE_ERROR; + $value = Xls\ErrorCode::lookup(ord($recordData[8])); + } elseif ( + (ord($recordData[6]) == 3) + && (ord($recordData[12]) == 255) + && (ord($recordData[13]) == 255) + ) { + // Formula result is a null string + $dataType = DataType::TYPE_NULL; + $value = ''; + } else { + // forumla result is a number, first 14 bytes like _NUMBER record + $dataType = DataType::TYPE_NUMERIC; + $value = self::extractNumber(substr($recordData, 6, 8)); + } + + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { + // add cell style + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + + // store the formula + if (!$isPartOfSharedFormula) { + // not part of shared formula + // add cell value. If we can read formula, populate with formula, otherwise just used cached value + try { + if ($this->version != self::XLS_BIFF8) { + throw new Exception('Not BIFF8. Can only read BIFF8 formulas'); + } + $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language + $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA); + } catch (PhpSpreadsheetException) { + $cell->setValueExplicit($value, $dataType); + } + } else { + if ($this->version == self::XLS_BIFF8) { + // do nothing at this point, formula id added later in the code + } else { + $cell->setValueExplicit($value, $dataType); + } + } + + // store the cached calculated value + $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC); + } + } + + /** + * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader, + * which usually contains relative references. + * These will be used to construct the formula in each shared formula part after the sheet is read. + */ + protected function readSharedFmla(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything + //$cellRange = substr($recordData, 0, 6); + //$cellRange = Xls\Biff5::readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax + + // offset: 6, size: 1; not used + + // offset: 7, size: 1; number of existing FORMULA records for this shared formula + //$no = ord($recordData[7]); + + // offset: 8, size: var; Binary token array of the shared formula + $formula = substr($recordData, 8); + + // at this point we only store the shared formula for later use + $this->sharedFormulas[$this->baseCell] = $formula; + } + + /** + * Read a STRING record from current stream position and advance the stream pointer to next record + * This record is used for storing result from FORMULA record when it is a string, and + * it occurs directly after the FORMULA record. + * + * @return string The string contents as UTF-8 + */ + protected function readString(): string + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringLong($recordData); + $value = $string['value']; + } else { + $string = $this->readByteStringLong($recordData); + $value = $string['value']; + } + /** @var string $value */ + + return $value; + } + + /** + * Read BOOLERR record + * This record represents a Boolean value or error value + * cell. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readBoolErr(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; row index + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; column index + $column = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($column + 1); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset: 4; size: 2; index to XF record + $xfIndex = self::getUInt2d($recordData, 4); + + // offset: 6; size: 1; the boolean value or error value + $boolErr = ord($recordData[6]); + + // offset: 7; size: 1; 0=boolean; 1=error + $isError = ord($recordData[7]); + + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + switch ($isError) { + case 0: // boolean + $value = (bool) $boolErr; + + // add cell value + $cell->setValueExplicit($value, DataType::TYPE_BOOL); + + break; + case 1: // error type + $value = Xls\ErrorCode::lookup($boolErr); + + // add cell value + $cell->setValueExplicit($value, DataType::TYPE_ERROR); + + break; + } + + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { + // add cell style + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + + /** + * Read MULBLANK record + * This record represents a cell range of empty cells. All + * cells are located in the same row. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readMulBlank(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; index to row + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to first column + $fc = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2 x nc; list of indexes to XF records + // add style information + if (!$this->readDataOnly && $this->readEmptyCells) { + for ($i = 0; $i < $length / 2 - 3; ++$i) { + $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i); + if (isset($this->mapCellXfIndex[$xfIndex])) { + $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + } + + // offset: 6; size 2; index to last column (not needed) + } + + /** + * Read LABEL record + * This record represents a cell that contains a string. In + * BIFF8 it is usually replaced by the LABELSST record. + * Excel still uses this record, if it copies unformatted + * text cells to the clipboard. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readLabel(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; index to row + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to column + $column = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($column + 1); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset: 4; size: 2; XF index + $xfIndex = self::getUInt2d($recordData, 4); + + // add cell value + // todo: what if string is very long? continue record + if ($this->version == self::XLS_BIFF8) { + $string = self::readUnicodeStringLong(substr($recordData, 6)); + $value = $string['value']; + } else { + $string = $this->readByteStringLong(substr($recordData, 6)); + $value = $string['value']; + } + /** @var string $value */ + if ($this->readEmptyCells || trim($value) !== '') { + $cell = $this->phpSheet->getCell($columnString . ($row + 1)); + $cell->setValueExplicit($value, DataType::TYPE_STRING); + + if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) { + // add cell style + $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + } + + /** + * Read BLANK record. + */ + protected function readBlank(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; row index + $row = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; col index + $col = self::getUInt2d($recordData, 2); + $columnString = Coordinate::stringFromColumnIndex($col + 1); + + // Read cell? + if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + // offset: 4; size: 2; XF index + $xfIndex = self::getUInt2d($recordData, 4); + + // add style information + if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) { + $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]); + } + } + } + + /** + * Read MSODRAWING record. + */ + protected function readMsoDrawing(): void + { + //$length = self::getUInt2d($this->data, $this->pos + 2); + + // get spliced record data + $splicedRecordData = $this->getSplicedRecordData(); + $recordData = $splicedRecordData['recordData']; + + $this->drawingData .= StringHelper::convertToString($recordData); + } + + /** + * Read OBJ record. + */ + protected function readObj(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly || $this->version != self::XLS_BIFF8) { + return; + } + + // recordData consists of an array of subrecords looking like this: + // ft: 2 bytes; ftCmo type (0x15) + // cb: 2 bytes; size in bytes of ftCmo data + // ot: 2 bytes; Object Type + // id: 2 bytes; Object id number + // grbit: 2 bytes; Option Flags + // data: var; subrecord data + + // for now, we are just interested in the second subrecord containing the object type + $ftCmoType = self::getUInt2d($recordData, 0); + $cbCmoSize = self::getUInt2d($recordData, 2); + $otObjType = self::getUInt2d($recordData, 4); + $idObjID = self::getUInt2d($recordData, 6); + $grbitOpts = self::getUInt2d($recordData, 6); + + $this->objs[] = [ + 'ftCmoType' => $ftCmoType, + 'cbCmoSize' => $cbCmoSize, + 'otObjType' => $otObjType, + 'idObjID' => $idObjID, + 'grbitOpts' => $grbitOpts, + ]; + $this->textObjRef = $idObjID; + } + + /** + * Read WINDOW2 record. + */ + protected function readWindow2(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; option flags + $options = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; index to first visible row + //$firstVisibleRow = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; index to first visible colum + //$firstVisibleColumn = self::getUInt2d($recordData, 4); + $zoomscaleInPageBreakPreview = 0; + $zoomscaleInNormalView = 0; + if ($this->version === self::XLS_BIFF8) { + // offset: 8; size: 2; not used + // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%) + // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%) + // offset: 14; size: 4; not used + if (!isset($recordData[10])) { + $zoomscaleInPageBreakPreview = 0; + } else { + $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10); + } + + if ($zoomscaleInPageBreakPreview === 0) { + $zoomscaleInPageBreakPreview = 60; + } + + if (!isset($recordData[12])) { + $zoomscaleInNormalView = 0; + } else { + $zoomscaleInNormalView = self::getUInt2d($recordData, 12); + } + + if ($zoomscaleInNormalView === 0) { + $zoomscaleInNormalView = 100; + } + } + + // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines + $showGridlines = (bool) ((0x0002 & $options) >> 1); + $this->phpSheet->setShowGridlines($showGridlines); + + // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers + $showRowColHeaders = (bool) ((0x0004 & $options) >> 2); + $this->phpSheet->setShowRowColHeaders($showRowColHeaders); + + // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen + $this->frozen = (bool) ((0x0008 & $options) >> 3); + + // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left + $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6)); + + // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active + $isActive = (bool) ((0x0400 & $options) >> 10); + if ($isActive) { + $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet)); + $this->activeSheetSet = true; + } + + // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view + $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11); + + //FIXME: set $firstVisibleRow and $firstVisibleColumn + + if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) { + //NOTE: this setting is inferior to page layout view(Excel2007-) + $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL; + $this->phpSheet->getSheetView()->setView($view); + if ($this->version === self::XLS_BIFF8) { + $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView; + $this->phpSheet->getSheetView()->setZoomScale($zoomScale); + $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView); + } + } + } + + /** + * Read PLV Record(Created by Excel2007 or upper). + */ + protected function readPageLayoutView(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; rt + //->ignore + //$rt = self::getUInt2d($recordData, 0); + // offset: 2; size: 2; grbitfr + //->ignore + //$grbitFrt = self::getUInt2d($recordData, 2); + // offset: 4; size: 8; reserved + //->ignore + + // offset: 12; size 2; zoom scale + $wScalePLV = self::getUInt2d($recordData, 12); + // offset: 14; size 2; grbit + $grbit = self::getUInt2d($recordData, 14); + + // decomprise grbit + $fPageLayoutView = $grbit & 0x01; + //$fRulerVisible = ($grbit >> 1) & 0x01; //no support + //$fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support + + if ($fPageLayoutView === 1) { + $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT); + $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT + } + //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW. + } + + /** + * Read SCL record. + */ + protected function readScl(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // offset: 0; size: 2; numerator of the view magnification + $numerator = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; numerator of the view magnification + $denumerator = self::getUInt2d($recordData, 2); + + // set the zoom scale (in percent) + $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator); + } + + /** + * Read PANE record. + */ + protected function readPane(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; position of vertical split + $px = self::getUInt2d($recordData, 0); + + // offset: 2; size: 2; position of horizontal split + $py = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; top most visible row in the bottom pane + $rwTop = self::getUInt2d($recordData, 4); + + // offset: 6; size: 2; first visible left column in the right pane + $colLeft = self::getUInt2d($recordData, 6); + + if ($this->frozen) { + // frozen panes + $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1); + $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1); + $this->phpSheet->freezePane($cell, $topLeftCell); + } + // unfrozen panes; split windows; not supported by PhpSpreadsheet core + } + } + + /** + * Read SELECTION record. There is one such record for each pane in the sheet. + */ + protected function readSelection(): string + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + $selectedCells = ''; + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 1; pane identifier + //$paneId = ord($recordData[0]); + + // offset: 1; size: 2; index to row of the active cell + //$r = self::getUInt2d($recordData, 1); + + // offset: 3; size: 2; index to column of the active cell + //$c = self::getUInt2d($recordData, 3); + + // offset: 5; size: 2; index into the following cell range list to the + // entry that contains the active cell + //$index = self::getUInt2d($recordData, 5); + + // offset: 7; size: var; cell range address list containing all selected cell ranges + $data = substr($recordData, 7); + $cellRangeAddressList = Xls\Biff5::readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax + + $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0]; + + // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!) + if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) { + $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells); + } + + // first row '1' + last row '65536' indicates that full column is selected + if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) { + $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells); + } + + // first column 'A' + last column 'IV' indicates that full row is selected + if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) { + $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells); + } + + $this->phpSheet->setSelectedCells($selectedCells); + } + + return $selectedCells; + } + + private function includeCellRangeFiltered(string $cellRangeAddress): bool + { + $includeCellRange = false; + $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress); + StringHelper::stringIncrement($rangeBoundaries[1][0]); + for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) { + for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) { + if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) { + $includeCellRange = true; + + break 2; + } + } + } + + return $includeCellRange; + } + + /** + * MERGEDCELLS. + * + * This record contains the addresses of merged cell ranges + * in the current sheet. + * + * -- "OpenOffice.org's Documentation of the Microsoft + * Excel File Format" + */ + protected function readMergedCells(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) { + $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($recordData); + foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) { + /** @var string $cellRangeAddress */ + if ( + (str_contains($cellRangeAddress, ':')) + && ($this->includeCellRangeFiltered($cellRangeAddress)) + ) { + $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE); + } + } + } + } + + /** + * Read HYPERLINK record. + */ + protected function readHyperLink(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer forward to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 8; cell range address of all cells containing this hyperlink + try { + $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed($recordData); + } catch (PhpSpreadsheetException) { + return; + } + + // offset: 8, size: 16; GUID of StdLink + + // offset: 24, size: 4; unknown value + + // offset: 28, size: 4; option flags + // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL + $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0; + + // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL + //$isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1; + + // bit: 2 (and 4); mask: 0x00000014; 0 = no description + $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2; + + // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text + $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3; + + // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame + $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7; + + // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name) + $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8; + + // offset within record data + $offset = 32; + + if ($hasDesc) { + // offset: 32; size: var; character count of description text + $dl = self::getInt4d($recordData, 32); + // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated + //$desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false); + $offset += 4 + 2 * $dl; + } + if ($hasFrame) { + $fl = self::getInt4d($recordData, $offset); + $offset += 4 + 2 * $fl; + } + + // detect type of hyperlink (there are 4 types) + $hyperlinkType = null; + + if ($isUNC) { + $hyperlinkType = 'UNC'; + } elseif (!$isFileLinkOrUrl) { + $hyperlinkType = 'workbook'; + } elseif (ord($recordData[$offset]) == 0x03) { + $hyperlinkType = 'local'; + } elseif (ord($recordData[$offset]) == 0xE0) { + $hyperlinkType = 'URL'; + } + + switch ($hyperlinkType) { + case 'URL': + // section 5.58.2: Hyperlink containing a URL + // e.g. http://example.org/index.php + + // offset: var; size: 16; GUID of URL Moniker + $offset += 16; + // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word + $us = self::getInt4d($recordData, $offset); + $offset += 4; + // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated + $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false); + $nullOffset = strpos($url, chr(0x00)); + if ($nullOffset) { + $url = substr($url, 0, $nullOffset); + } + $url .= $hasText ? '#' : ''; + $offset += $us; + + break; + case 'local': + // section 5.58.3: Hyperlink to local file + // examples: + // mydoc.txt + // ../../somedoc.xls#Sheet!A1 + + // offset: var; size: 16; GUI of File Moniker + $offset += 16; + + // offset: var; size: 2; directory up-level count. + $upLevelCount = self::getUInt2d($recordData, $offset); + $offset += 2; + + // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word + $sl = self::getInt4d($recordData, $offset); + $offset += 4; + + // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string) + $shortenedFilePath = substr($recordData, $offset, $sl); + $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true); + $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero + + $offset += $sl; + + // offset: var; size: 24; unknown sequence + $offset += 24; + + // extended file path + // offset: var; size: 4; size of the following file link field including string lenth mark + $sz = self::getInt4d($recordData, $offset); + $offset += 4; + + $extendedFilePath = ''; + // only present if $sz > 0 + if ($sz > 0) { + // offset: var; size: 4; size of the character array of the extended file path and name + $xl = self::getInt4d($recordData, $offset); + $offset += 4; + + // offset: var; size 2; unknown + $offset += 2; + + // offset: var; size $xl; character array of the extended file path and name. + $extendedFilePath = substr($recordData, $offset, $xl); + $extendedFilePath = self::encodeUTF16($extendedFilePath, false); + $offset += $xl; + } + + // construct the path + $url = str_repeat('..\\', $upLevelCount); + $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available + $url .= $hasText ? '#' : ''; + + break; + case 'UNC': + // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path + // todo: implement + return; + case 'workbook': + // section 5.58.5: Hyperlink to the Current Workbook + // e.g. Sheet2!B1:C2, stored in text mark field + $url = 'sheet://'; + + break; + default: + return; + } + + if ($hasText) { + // offset: var; size: 4; character count of text mark including trailing zero word + $tl = self::getInt4d($recordData, $offset); + $offset += 4; + // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated + $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false); + $url .= $text; + } + + // apply the hyperlink to all the relevant cells + foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) { + $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url); + } + } + } + + /** + * Read DATAVALIDATIONS record. + */ + protected function readDataValidations(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer forward to next record + $this->pos += 4 + $length; + } + + /** + * Read DATAVALIDATION record. + */ + protected function readDataValidation(): void + { + (new Xls\DataValidationHelper())->readDataValidation2($this); + } + + /** + * Read SHEETLAYOUT record. Stores sheet tab color information. + */ + protected function readSheetLayout(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if (!$this->readDataOnly) { + // offset: 0; size: 2; repeated record identifier 0x0862 + + // offset: 2; size: 10; not used + + // offset: 12; size: 4; size of record data + // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?) + $sz = self::getInt4d($recordData, 12); + + switch ($sz) { + case 0x14: + // offset: 16; size: 2; color index for sheet tab + $colorIndex = self::getUInt2d($recordData, 16); + /** @var string[] */ + $color = Xls\Color::map($colorIndex, $this->palette, $this->version); + $this->phpSheet->getTabColor()->setRGB($color['rgb']); + + break; + case 0x28: + // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007 + return; + } + } + } + + /** + * Read SHEETPROTECTION record (FEATHEADR). + */ + protected function readSheetProtection(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; repeated record header + + // offset: 2; size: 2; FRT cell reference flag (=0 currently) + + // offset: 4; size: 8; Currently not used and set to 0 + + // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag) + $isf = self::getUInt2d($recordData, 12); + if ($isf != 2) { + return; + } + + // offset: 14; size: 1; =1 since this is a feat header + + // offset: 15; size: 4; size of rgbHdrSData + + // rgbHdrSData, assume "Enhanced Protection" + // offset: 19; size: 2; option flags + $options = self::getUInt2d($recordData, 19); + + // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects + // Note - do not negate $bool + $bool = (0x0001 & $options) >> 0; + $this->phpSheet->getProtection()->setObjects((bool) $bool); + + // bit: 1; mask 0x0002; edit scenarios + // Note - do not negate $bool + $bool = (0x0002 & $options) >> 1; + $this->phpSheet->getProtection()->setScenarios((bool) $bool); + + // bit: 2; mask 0x0004; format cells + $bool = (0x0004 & $options) >> 2; + $this->phpSheet->getProtection()->setFormatCells(!$bool); + + // bit: 3; mask 0x0008; format columns + $bool = (0x0008 & $options) >> 3; + $this->phpSheet->getProtection()->setFormatColumns(!$bool); + + // bit: 4; mask 0x0010; format rows + $bool = (0x0010 & $options) >> 4; + $this->phpSheet->getProtection()->setFormatRows(!$bool); + + // bit: 5; mask 0x0020; insert columns + $bool = (0x0020 & $options) >> 5; + $this->phpSheet->getProtection()->setInsertColumns(!$bool); + + // bit: 6; mask 0x0040; insert rows + $bool = (0x0040 & $options) >> 6; + $this->phpSheet->getProtection()->setInsertRows(!$bool); + + // bit: 7; mask 0x0080; insert hyperlinks + $bool = (0x0080 & $options) >> 7; + $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool); + + // bit: 8; mask 0x0100; delete columns + $bool = (0x0100 & $options) >> 8; + $this->phpSheet->getProtection()->setDeleteColumns(!$bool); + + // bit: 9; mask 0x0200; delete rows + $bool = (0x0200 & $options) >> 9; + $this->phpSheet->getProtection()->setDeleteRows(!$bool); + + // bit: 10; mask 0x0400; select locked cells + // Note that this is opposite of most of above. + $bool = (0x0400 & $options) >> 10; + $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool); + + // bit: 11; mask 0x0800; sort cell range + $bool = (0x0800 & $options) >> 11; + $this->phpSheet->getProtection()->setSort(!$bool); + + // bit: 12; mask 0x1000; auto filter + $bool = (0x1000 & $options) >> 12; + $this->phpSheet->getProtection()->setAutoFilter(!$bool); + + // bit: 13; mask 0x2000; pivot tables + $bool = (0x2000 & $options) >> 13; + $this->phpSheet->getProtection()->setPivotTables(!$bool); + + // bit: 14; mask 0x4000; select unlocked cells + // Note that this is opposite of most of above. + $bool = (0x4000 & $options) >> 14; + $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool); + + // offset: 21; size: 2; not used + } + + /** + * Read RANGEPROTECTION record + * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification, + * where it is referred to as FEAT record. + */ + protected function readRangeProtection(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer to next record + $this->pos += 4 + $length; + + // local pointer in record data + $offset = 0; + + if (!$this->readDataOnly) { + $offset += 12; + + // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag + $isf = self::getUInt2d($recordData, 12); + if ($isf != 2) { + // we only read FEAT records of type 2 + return; + } + $offset += 2; + + $offset += 5; + + // offset: 19; size: 2; count of ref ranges this feature is on + $cref = self::getUInt2d($recordData, 19); + $offset += 2; + + $offset += 6; + + // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record) + $cellRanges = []; + for ($i = 0; $i < $cref; ++$i) { + try { + $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8)); + } catch (PhpSpreadsheetException) { + return; + } + $cellRanges[] = $cellRange; + $offset += 8; + } + + // offset: var; size: var; variable length of feature specific data + //$rgbFeat = substr($recordData, $offset); + $offset += 4; + + // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit) + $wPassword = self::getInt4d($recordData, $offset); + $offset += 4; + + // Apply range protection to sheet + if ($cellRanges) { + $this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true); + } + } + } + + /** + * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record + * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented. + * In this case, we must treat the CONTINUE record as a MSODRAWING record. + */ + protected function readContinue(): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // check if we are reading drawing data + // this is in case a free CONTINUE record occurs in other circumstances we are unaware of + if ($this->drawingData == '') { + // move stream pointer to next record + $this->pos += 4 + $length; + + return; + } + + // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data + if ($length < 4) { + // move stream pointer to next record + $this->pos += 4 + $length; + + return; + } + + // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record + // look inside CONTINUE record to see if it looks like a part of an Escher stream + // we know that Escher stream may be split at least at + // 0xF003 MsofbtSpgrContainer + // 0xF004 MsofbtSpContainer + // 0xF00D MsofbtClientTextbox + $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more + + $splitPoint = self::getUInt2d($recordData, 2); + if (in_array($splitPoint, $validSplitPoints)) { + // get spliced record data (and move pointer to next record) + $splicedRecordData = $this->getSplicedRecordData(); + $this->drawingData .= StringHelper::convertToString($splicedRecordData['recordData']); + + return; + } + + // move stream pointer to next record + $this->pos += 4 + $length; + } + + /** + * Reads a record from current position in data stream and continues reading data as long as CONTINUE + * records are found. Splices the record data pieces and returns the combined string as if record data + * is in one piece. + * Moves to next current position in data stream to start of next record different from a CONtINUE record. + * + * @return mixed[] + */ + private function getSplicedRecordData(): array + { + $data = ''; + $spliceOffsets = []; + + $i = 0; + $spliceOffsets[0] = 0; + + do { + ++$i; + + // offset: 0; size: 2; identifier + //$identifier = self::getUInt2d($this->data, $this->pos); + // offset: 2; size: 2; length + $length = self::getUInt2d($this->data, $this->pos + 2); + $data .= $this->readRecordData($this->data, $this->pos + 4, $length); + + $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length; + + $this->pos += 4 + $length; + $nextIdentifier = self::getUInt2d($this->data, $this->pos); + } while ($nextIdentifier == self::XLS_TYPE_CONTINUE); + + return [ + 'recordData' => $data, + 'spliceOffsets' => $spliceOffsets, + ]; + } + + /** + * Convert formula structure into human readable Excel formula like 'A3+A5*5'. + * + * @param string $formulaStructure The complete binary data for the formula + * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas + * + * @return string Human readable formula + */ + protected function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string + { + // offset: 0; size: 2; size of the following formula data + $sz = self::getUInt2d($formulaStructure, 0); + + // offset: 2; size: sz + $formulaData = substr($formulaStructure, 2, $sz); + + // offset: 2 + sz; size: variable (optional) + if (strlen($formulaStructure) > 2 + $sz) { + $additionalData = substr($formulaStructure, 2 + $sz); + } else { + $additionalData = ''; + } + + return $this->getFormulaFromData($formulaData, $additionalData, $baseCell); + } + + /** + * Take formula data and additional data for formula and return human readable formula. + * + * @param string $formulaData The binary data for the formula itself + * @param string $additionalData Additional binary data going with the formula + * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas + * + * @return string Human readable formula + */ + private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string + { + // start parsing the formula data + $tokens = []; + + while ($formulaData !== '' && $token = $this->getNextToken($formulaData, $baseCell)) { + $tokens[] = $token; + /** @var int[] $token */ + $formulaData = substr($formulaData, $token['size']); + } + + $formulaString = $this->createFormulaFromTokens($tokens, $additionalData); + + return $formulaString; + } + + /** + * Take array of tokens together with additional data for formula and return human readable formula. + * + * @param mixed[][] $tokens + * @param string $additionalData Additional binary data going with the formula + * + * @return string Human readable formula + */ + private function createFormulaFromTokens(array $tokens, string $additionalData): string + { + // empty formula? + if (empty($tokens)) { + return ''; + } + + $formulaStrings = []; + foreach ($tokens as $token) { + // initialize spaces + $space0 = $space0 ?? ''; // spaces before next token, not tParen + $space1 = $space1 ?? ''; // carriage returns before next token, not tParen + $space2 = $space2 ?? ''; // spaces before opening parenthesis + $space3 = $space3 ?? ''; // carriage returns before opening parenthesis + $space4 = $space4 ?? ''; // spaces before closing parenthesis + $space5 = $space5 ?? ''; // carriage returns before closing parenthesis + /** @var string */ + $tokenData = $token['data'] ?? ''; + switch ($token['name']) { + case 'tAdd': // addition + case 'tConcat': // addition + case 'tDiv': // division + case 'tEQ': // equality + case 'tGE': // greater than or equal + case 'tGT': // greater than + case 'tIsect': // intersection + case 'tLE': // less than or equal + case 'tList': // less than or equal + case 'tLT': // less than + case 'tMul': // multiplication + case 'tNE': // multiplication + case 'tPower': // power + case 'tRange': // range + case 'tSub': // subtraction + $op2 = array_pop($formulaStrings); + $op1 = array_pop($formulaStrings); + $formulaStrings[] = "$op1$space1$space0{$tokenData}$op2"; + unset($space0, $space1); + + break; + case 'tUplus': // unary plus + case 'tUminus': // unary minus + $op = array_pop($formulaStrings); + $formulaStrings[] = "$space1$space0{$tokenData}$op"; + unset($space0, $space1); + + break; + case 'tPercent': // percent sign + $op = array_pop($formulaStrings); + $formulaStrings[] = "$op$space1$space0{$tokenData}"; + unset($space0, $space1); + + break; + case 'tAttrVolatile': // indicates volatile function + case 'tAttrIf': + case 'tAttrSkip': + case 'tAttrChoose': + // token is only important for Excel formula evaluator + // do nothing + break; + case 'tAttrSpace': // space / carriage return + // space will be used when next token arrives, do not alter formulaString stack + /** @var string[][] $token */ + switch ($token['data']['spacetype']) { + case 'type0': + $space0 = str_repeat(' ', (int) $token['data']['spacecount']); + + break; + case 'type1': + $space1 = str_repeat("\n", (int) $token['data']['spacecount']); + + break; + case 'type2': + $space2 = str_repeat(' ', (int) $token['data']['spacecount']); + + break; + case 'type3': + $space3 = str_repeat("\n", (int) $token['data']['spacecount']); + + break; + case 'type4': + $space4 = str_repeat(' ', (int) $token['data']['spacecount']); + + break; + case 'type5': + $space5 = str_repeat("\n", (int) $token['data']['spacecount']); + + break; + } + + break; + case 'tAttrSum': // SUM function with one parameter + $op = array_pop($formulaStrings); + $formulaStrings[] = "{$space1}{$space0}SUM($op)"; + unset($space0, $space1); + + break; + case 'tFunc': // function with fixed number of arguments + case 'tFuncV': // function with variable number of arguments + /** @var string[] */ + $temp1 = $token['data']; + $temp2 = $temp1['function']; + if ($temp2 != '') { + // normal function + $ops = []; // array of operators + $temp3 = (int) $temp1['args']; + for ($i = 0; $i < $temp3; ++$i) { + $ops[] = array_pop($formulaStrings); + } + $ops = array_reverse($ops); + $formulaStrings[] = "$space1$space0{$temp2}(" . implode(',', $ops) . ')'; + unset($space0, $space1); + } else { + // add-in function + $ops = []; // array of operators + /** @var int[] */ + $temp = $token['data']; + for ($i = 0; $i < $temp['args'] - 1; ++$i) { + $ops[] = array_pop($formulaStrings); + } + $ops = array_reverse($ops); + $function = array_pop($formulaStrings); + $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')'; + unset($space0, $space1); + } + + break; + case 'tParen': // parenthesis + $expression = array_pop($formulaStrings); + $formulaStrings[] = "$space3$space2($expression$space5$space4)"; + unset($space2, $space3, $space4, $space5); + + break; + case 'tArray': // array constant + $constantArray = Xls\Biff8::readBIFF8ConstantArray($additionalData); + $formulaStrings[] = $space1 . $space0 . $constantArray['value']; + $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data + unset($space0, $space1); + + break; + case 'tMemArea': + // bite off chunk of additional data + $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($additionalData); + $additionalData = substr($additionalData, $cellRangeAddressList['size']); + $formulaStrings[] = "$space1$space0{$tokenData}"; + unset($space0, $space1); + + break; + case 'tArea': // cell range address + case 'tBool': // boolean + case 'tErr': // error code + case 'tInt': // integer + case 'tMemErr': + case 'tMemFunc': + case 'tMissArg': + case 'tName': + case 'tNameX': + case 'tNum': // number + case 'tRef': // single cell reference + case 'tRef3d': // 3d cell reference + case 'tArea3d': // 3d cell range reference + case 'tRefN': + case 'tAreaN': + case 'tStr': // string + $formulaStrings[] = "$space1$space0{$tokenData}"; + unset($space0, $space1); + + break; + } + } + $formulaString = $formulaStrings[0]; + + return $formulaString; + } + + /** + * Fetch next token from binary formula data. + * + * @param string $formulaData Formula data + * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas + * + * @return mixed[] + */ + private function getNextToken(string $formulaData, string $baseCell = 'A1'): array + { + // offset: 0; size: 1; token id + $id = ord($formulaData[0]); // token id + $name = false; // initialize token name + + switch ($id) { + case 0x03: + $name = 'tAdd'; + $size = 1; + $data = '+'; + + break; + case 0x04: + $name = 'tSub'; + $size = 1; + $data = '-'; + + break; + case 0x05: + $name = 'tMul'; + $size = 1; + $data = '*'; + + break; + case 0x06: + $name = 'tDiv'; + $size = 1; + $data = '/'; + + break; + case 0x07: + $name = 'tPower'; + $size = 1; + $data = '^'; + + break; + case 0x08: + $name = 'tConcat'; + $size = 1; + $data = '&'; + + break; + case 0x09: + $name = 'tLT'; + $size = 1; + $data = '<'; + + break; + case 0x0A: + $name = 'tLE'; + $size = 1; + $data = '<='; + + break; + case 0x0B: + $name = 'tEQ'; + $size = 1; + $data = '='; + + break; + case 0x0C: + $name = 'tGE'; + $size = 1; + $data = '>='; + + break; + case 0x0D: + $name = 'tGT'; + $size = 1; + $data = '>'; + + break; + case 0x0E: + $name = 'tNE'; + $size = 1; + $data = '<>'; + + break; + case 0x0F: + $name = 'tIsect'; + $size = 1; + $data = ' '; + + break; + case 0x10: + $name = 'tList'; + $size = 1; + $data = ','; + + break; + case 0x11: + $name = 'tRange'; + $size = 1; + $data = ':'; + + break; + case 0x12: + $name = 'tUplus'; + $size = 1; + $data = '+'; + + break; + case 0x13: + $name = 'tUminus'; + $size = 1; + $data = '-'; + + break; + case 0x14: + $name = 'tPercent'; + $size = 1; + $data = '%'; + + break; + case 0x15: // parenthesis + $name = 'tParen'; + $size = 1; + $data = null; + + break; + case 0x16: // missing argument + $name = 'tMissArg'; + $size = 1; + $data = ''; + + break; + case 0x17: // string + $name = 'tStr'; + // offset: 1; size: var; Unicode string, 8-bit string length + $string = self::readUnicodeStringShort(substr($formulaData, 1)); + $size = 1 + $string['size']; + $data = self::UTF8toExcelDoubleQuoted($string['value']); + + break; + case 0x19: // Special attribute + // offset: 1; size: 1; attribute type flags: + switch (ord($formulaData[1])) { + case 0x01: + $name = 'tAttrVolatile'; + $size = 4; + $data = null; + + break; + case 0x02: + $name = 'tAttrIf'; + $size = 4; + $data = null; + + break; + case 0x04: + $name = 'tAttrChoose'; + // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1) + $nc = self::getUInt2d($formulaData, 2); + // offset: 4; size: 2 * $nc + // offset: 4 + 2 * $nc; size: 2 + $size = 2 * $nc + 6; + $data = null; + + break; + case 0x08: + $name = 'tAttrSkip'; + $size = 4; + $data = null; + + break; + case 0x10: + $name = 'tAttrSum'; + $size = 4; + $data = null; + + break; + case 0x40: + case 0x41: + $name = 'tAttrSpace'; + $size = 4; + // offset: 2; size: 2; space type and position + $spacetype = match (ord($formulaData[2])) { + 0x00 => 'type0', + 0x01 => 'type1', + 0x02 => 'type2', + 0x03 => 'type3', + 0x04 => 'type4', + 0x05 => 'type5', + default => throw new Exception('Unrecognized space type in tAttrSpace token'), + }; + // offset: 3; size: 1; number of inserted spaces/carriage returns + $spacecount = ord($formulaData[3]); + + $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount]; + + break; + default: + throw new Exception('Unrecognized attribute flag in tAttr token'); + } + + break; + case 0x1C: // error code + // offset: 1; size: 1; error code + $name = 'tErr'; + $size = 2; + $data = Xls\ErrorCode::lookup(ord($formulaData[1])); + + break; + case 0x1D: // boolean + // offset: 1; size: 1; 0 = false, 1 = true; + $name = 'tBool'; + $size = 2; + $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE'; + + break; + case 0x1E: // integer + // offset: 1; size: 2; unsigned 16-bit integer + $name = 'tInt'; + $size = 3; + $data = self::getUInt2d($formulaData, 1); + + break; + case 0x1F: // number + // offset: 1; size: 8; + $name = 'tNum'; + $size = 9; + $data = self::extractNumber(substr($formulaData, 1)); + $data = str_replace(',', '.', (string) $data); // in case non-English locale + + break; + case 0x20: // array constant + case 0x40: + case 0x60: + // offset: 1; size: 7; not used + $name = 'tArray'; + $size = 8; + $data = null; + + break; + case 0x21: // function with fixed number of arguments + case 0x41: + case 0x61: + $name = 'tFunc'; + $size = 3; + // offset: 1; size: 2; index to built-in sheet function + $mapping = Xls\Mappings::TFUNC_MAPPINGS[self::getUInt2d($formulaData, 1)] ?? null; + if ($mapping === null) { + throw new Exception('Unrecognized function in formula'); + } + $data = ['function' => $mapping[0], 'args' => $mapping[1]]; + + break; + case 0x22: // function with variable number of arguments + case 0x42: + case 0x62: + $name = 'tFuncV'; + $size = 4; + // offset: 1; size: 1; number of arguments + $args = ord($formulaData[1]); + // offset: 2: size: 2; index to built-in sheet function + $index = self::getUInt2d($formulaData, 2); + $function = Xls\Mappings::TFUNCV_MAPPINGS[$index] ?? null; + if ($function === null) { + throw new Exception('Unrecognized function in formula'); + } + $data = ['function' => $function, 'args' => $args]; + + break; + case 0x23: // index to defined name + case 0x43: + case 0x63: + $name = 'tName'; + $size = 5; + // offset: 1; size: 2; one-based index to definedname record + $definedNameIndex = self::getUInt2d($formulaData, 1) - 1; + // offset: 2; size: 2; not used + /** @var string[] */ + $data = $this->definedname[$definedNameIndex]['name'] ?? ''; //* @phpstan-ignore-line + + break; + case 0x24: // single cell reference e.g. A5 + case 0x44: + case 0x64: + $name = 'tRef'; + $size = 5; + $data = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 1, 4)); + + break; + case 0x25: // cell range reference to cells in the same sheet (2d) + case 0x45: + case 0x65: + $name = 'tArea'; + $size = 9; + $data = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 1, 8)); + + break; + case 0x26: // Constant reference sub-expression + case 0x46: + case 0x66: + $name = 'tMemArea'; + // offset: 1; size: 4; not used + // offset: 5; size: 2; size of the following subexpression + $subSize = self::getUInt2d($formulaData, 5); + $size = 7 + $subSize; + $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize)); + + break; + case 0x27: // Deleted constant reference sub-expression + case 0x47: + case 0x67: + $name = 'tMemErr'; + // offset: 1; size: 4; not used + // offset: 5; size: 2; size of the following subexpression + $subSize = self::getUInt2d($formulaData, 5); + $size = 7 + $subSize; + $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize)); + + break; + case 0x29: // Variable reference sub-expression + case 0x49: + case 0x69: + $name = 'tMemFunc'; + // offset: 1; size: 2; size of the following sub-expression + $subSize = self::getUInt2d($formulaData, 1); + $size = 3 + $subSize; + $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize)); + + break; + case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places + case 0x4C: + case 0x6C: + $name = 'tRefN'; + $size = 5; + $data = Xls\Biff8::readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell); + + break; + case 0x2D: // Relative 2d range reference + case 0x4D: + case 0x6D: + $name = 'tAreaN'; + $size = 9; + $data = Xls\Biff8::readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell); + + break; + case 0x39: // External name + case 0x59: + case 0x79: + $name = 'tNameX'; + $size = 7; + // offset: 1; size: 2; index to REF entry in EXTERNSHEET record + // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record + $index = self::getUInt2d($formulaData, 3); + // assume index is to EXTERNNAME record + $data = $this->externalNames[$index - 1]['name'] ?? ''; + + // offset: 5; size: 2; not used + break; + case 0x3A: // 3d reference to cell + case 0x5A: + case 0x7A: + $name = 'tRef3d'; + $size = 7; + + try { + // offset: 1; size: 2; index to REF entry + $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1)); + // offset: 3; size: 4; cell address + $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 3, 4)); + + $data = "$sheetRange!$cellAddress"; + } catch (PhpSpreadsheetException) { + // deleted sheet reference + $data = '#REF!'; + } + + break; + case 0x3B: // 3d reference to cell range + case 0x5B: + case 0x7B: + $name = 'tArea3d'; + $size = 11; + + try { + // offset: 1; size: 2; index to REF entry + $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1)); + // offset: 3; size: 8; cell address + $cellRangeAddress = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 3, 8)); + + $data = "$sheetRange!$cellRangeAddress"; + } catch (PhpSpreadsheetException) { + // deleted sheet reference + $data = '#REF!'; + } + + break; + // Unknown cases // don't know how to deal with + default: + throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula'); + } + + return [ + 'id' => $id, + 'name' => $name, + 'size' => $size, + 'data' => $data, + ]; + } + + /** + * Get a sheet range like Sheet1:Sheet3 from REF index + * Note: If there is only one sheet in the range, one gets e.g Sheet1 + * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets, + * in which case an Exception is thrown. + */ + protected function readSheetRangeByRefIndex(int $index): string|false + { + if (isset($this->ref[$index])) { + $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type']; + + switch ($type) { + case 'internal': + // check if we have a deleted 3d reference + if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) { + throw new Exception('Deleted sheet reference'); + } + + // we have normal sheet range (collapsed or uncollapsed) + $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name']; + $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name']; + + if ($firstSheetName == $lastSheetName) { + // collapsed sheet range + $sheetRange = $firstSheetName; + } else { + $sheetRange = "$firstSheetName:$lastSheetName"; + } + + // escape the single-quotes + $sheetRange = str_replace("'", "''", $sheetRange); + + // if there are special characters, we need to enclose the range in single-quotes + // todo: check if we have identified the whole set of special characters + // it seems that the following characters are not accepted for sheet names + // and we may assume that they are not present: []*/:\? + if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) { + $sheetRange = "'$sheetRange'"; + } + + return $sheetRange; + default: + // TODO: external sheet support + throw new Exception('Xls reader only supports internal sheets in formulas'); + } + } + + return false; + } + + /** + * Read byte string (8-bit string length) + * OpenOffice documentation: 2.5.2. + * + * @return array{value: mixed, size: int} + */ + protected function readByteStringShort(string $subData): array + { + // offset: 0; size: 1; length of the string (character count) + $ln = ord($subData[0]); + + // offset: 1: size: var; character array (8-bit characters) + $value = $this->decodeCodepage(substr($subData, 1, $ln)); + + return [ + 'value' => $value, + 'size' => 1 + $ln, // size in bytes of data structure + ]; + } + + /** + * Read byte string (16-bit string length) + * OpenOffice documentation: 2.5.2. + * + * @return array{value: mixed, size: int} + */ + protected function readByteStringLong(string $subData): array + { + // offset: 0; size: 2; length of the string (character count) + $ln = self::getUInt2d($subData, 0); + + // offset: 2: size: var; character array (8-bit characters) + $value = $this->decodeCodepage(substr($subData, 2)); + + //return $string; + return [ + 'value' => $value, + 'size' => 2 + $ln, // size in bytes of data structure + ]; + } + + protected function parseRichText(string $is): RichText + { + $value = new RichText(); + $value->createText($is); + + return $value; + } + + /** + * Phpstan 1.4.4 complains that this property is never read. + * So, we might be able to get rid of it altogether. + * For now, however, this function makes it readable, + * which satisfies Phpstan. + * + * @return mixed[] + * + * @codeCoverageIgnore + */ + public function getMapCellStyleXfIndex(): array + { + return $this->mapCellStyleXfIndex; + } + + /** + * Parse conditional formatting blocks. + * + * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE + * + * @return mixed[] + */ + protected function readCFHeader(): array + { + return (new Xls\ConditionalFormatting())->readCFHeader2($this); + } + + /** @param string[] $cellRangeAddresses */ + protected function readCFRule(array $cellRangeAddresses): void + { + (new Xls\ConditionalFormatting())->readCFRule2($cellRangeAddresses, $this); + } + + public function getVersion(): int + { + return $this->version; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Biff5.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Biff5.php new file mode 100644 index 0000000..61ff9ee --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Biff5.php @@ -0,0 +1,71 @@ + $lr || $fc > $lc) { + throw new ReaderException('Not a cell range address'); + } + + // column index to letter + $fc = Coordinate::stringFromColumnIndex($fc + 1); + $lc = Coordinate::stringFromColumnIndex($lc + 1); + + if ($fr == $lr && $fc == $lc) { + return "$fc$fr"; + } + + return "$fc$fr:$lc$lr"; + } + + /** + * Read BIFF5 cell range address list + * section 2.5.15. + * + * @return array{size: int, cellRangeAddresses: string[]} + */ + public static function readBIFF5CellRangeAddressList(string $subData): array + { + $cellRangeAddresses = []; + + // offset: 0; size: 2; number of the following cell range addresses + $nm = self::getUInt2d($subData, 0); + + $offset = 2; + // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses + for ($i = 0; $i < $nm; ++$i) { + $cellRangeAddresses[] = self::readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6)); + $offset += 6; + } + + return [ + 'size' => 2 + 6 * $nm, + 'cellRangeAddresses' => $cellRangeAddresses, + ]; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Biff8.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Biff8.php new file mode 100644 index 0000000..6bb8472 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Biff8.php @@ -0,0 +1,371 @@ + '{1,2;3,4}', 'size' => 40] + * section 2.5.8. + * + * @return array{value: string, size: int} + */ + protected static function readBIFF8ConstantArray(string $arrayData): array + { + // offset: 0; size: 1; number of columns decreased by 1 + $nc = ord($arrayData[0]); + + // offset: 1; size: 2; number of rows decreased by 1 + $nr = self::getUInt2d($arrayData, 1); + $size = 3; // initialize + $arrayData = substr($arrayData, 3); + + // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values + $matrixChunks = []; + for ($r = 1; $r <= $nr + 1; ++$r) { + $items = []; + for ($c = 1; $c <= $nc + 1; ++$c) { + $constant = self::readBIFF8Constant($arrayData); + $items[] = $constant['value']; + $arrayData = substr($arrayData, $constant['size']); + $size += $constant['size']; + } + $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"' + } + $matrix = '{' . implode(';', $matrixChunks) . '}'; + + return [ + 'value' => $matrix, + 'size' => $size, + ]; + } + + /** + * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value' + * section 2.5.7 + * returns e.g. ['value' => '5', 'size' => 9]. + * + * @return array{value: bool|float|int|string, size: int} + */ + private static function readBIFF8Constant(string $valueData): array + { + // offset: 0; size: 1; identifier for type of constant + $identifier = ord($valueData[0]); + + switch ($identifier) { + case 0x00: // empty constant (what is this?) + $value = ''; + $size = 9; + + break; + case 0x01: // number + // offset: 1; size: 8; IEEE 754 floating-point value + $value = self::extractNumber(substr($valueData, 1, 8)); + $size = 9; + + break; + case 0x02: // string value + // offset: 1; size: var; Unicode string, 16-bit string length + $string = self::readUnicodeStringLong(substr($valueData, 1)); + $value = '"' . $string['value'] . '"'; + $size = 1 + $string['size']; + + break; + case 0x04: // boolean + // offset: 1; size: 1; 0 = FALSE, 1 = TRUE + if (ord($valueData[1])) { + $value = 'TRUE'; + } else { + $value = 'FALSE'; + } + $size = 9; + + break; + case 0x10: // error code + // offset: 1; size: 1; error code + $value = ErrorCode::lookup(ord($valueData[1])); + $size = 9; + + break; + default: + throw new ReaderException('Unsupported BIFF8 constant'); + } + + return [ + 'value' => $value, + 'size' => $size, + ]; + } + + /** + * Read BIFF8 cell range address list + * section 2.5.15. + * + * @return array{size: int, cellRangeAddresses: mixed[]} + */ + public static function readBIFF8CellRangeAddressList(string $subData): array + { + $cellRangeAddresses = []; + + // offset: 0; size: 2; number of the following cell range addresses + $nm = self::getUInt2d($subData, 0); + + $offset = 2; + // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses + for ($i = 0; $i < $nm; ++$i) { + $cellRangeAddresses[] = self::readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8)); + $offset += 8; + } + + return [ + 'size' => 2 + 8 * $nm, + 'cellRangeAddresses' => $cellRangeAddresses, + ]; + } + + /** + * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2' + * section 3.3.4. + */ + protected static function readBIFF8CellAddress(string $cellAddressStructure): string + { + // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) + $row = self::getUInt2d($cellAddressStructure, 0) + 1; + + // offset: 2; size: 2; index to column or column offset + relative flags + // bit: 7-0; mask 0x00FF; column index + $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1); + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) { + $column = '$' . $column; + } + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) { + $row = '$' . $row; + } + + return $column . $row; + } + + /** + * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column + * to indicate offsets from a base cell + * section 3.3.4. + * + * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas + */ + protected static function readBIFF8CellAddressB(string $cellAddressStructure, string $baseCell = 'A1'): string + { + [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell); + $baseCol = Coordinate::columnIndexFromString($baseCol) - 1; + $baseRow = (int) $baseRow; + + // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) + $rowIndex = self::getUInt2d($cellAddressStructure, 0); + $row = self::getUInt2d($cellAddressStructure, 0) + 1; + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) { + // offset: 2; size: 2; index to column or column offset + relative flags + // bit: 7-0; mask 0x00FF; column index + $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2); + + $column = Coordinate::stringFromColumnIndex($colIndex + 1); + $column = '$' . $column; + } else { + // offset: 2; size: 2; index to column or column offset + relative flags + // bit: 7-0; mask 0x00FF; column index + $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2); + $colIndex = $baseCol + $relativeColIndex; + $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256; + $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256; + $column = Coordinate::stringFromColumnIndex($colIndex + 1); + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) { + $row = '$' . $row; + } else { + $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536; + $row = $baseRow + $rowIndex; + } + + return $column . $row; + } + + /** + * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1' + * always fixed range + * section 2.5.14. + */ + protected static function readBIFF8CellRangeAddressFixed(string $subData): string + { + // offset: 0; size: 2; index to first row + $fr = self::getUInt2d($subData, 0) + 1; + + // offset: 2; size: 2; index to last row + $lr = self::getUInt2d($subData, 2) + 1; + + // offset: 4; size: 2; index to first column + $fc = self::getUInt2d($subData, 4); + + // offset: 6; size: 2; index to last column + $lc = self::getUInt2d($subData, 6); + + // check values + if ($fr > $lr || $fc > $lc) { + throw new ReaderException('Not a cell range address'); + } + + // column index to letter + $fc = Coordinate::stringFromColumnIndex($fc + 1); + $lc = Coordinate::stringFromColumnIndex($lc + 1); + + if ($fr == $lr && $fc == $lc) { + return "$fc$fr"; + } + + return "$fc$fr:$lc$lr"; + } + + /** + * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6' + * there are flags indicating whether column/row index is relative + * section 3.3.4. + */ + protected static function readBIFF8CellRangeAddress(string $subData): string + { + // todo: if cell range is just a single cell, should this function + // not just return e.g. 'A1' and not 'A1:A1' ? + + // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767)) + $fr = self::getUInt2d($subData, 0) + 1; + + // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767)) + $lr = self::getUInt2d($subData, 2) + 1; + + // offset: 4; size: 2; index to first column or column offset + relative flags + + // bit: 7-0; mask 0x00FF; column index + $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1); + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 4))) { + $fc = '$' . $fc; + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 4))) { + $fr = '$' . $fr; + } + + // offset: 6; size: 2; index to last column or column offset + relative flags + + // bit: 7-0; mask 0x00FF; column index + $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1); + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 6))) { + $lc = '$' . $lc; + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 6))) { + $lr = '$' . $lr; + } + + return "$fc$fr:$lc$lr"; + } + + /** + * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column + * to indicate offsets from a base cell + * section 3.3.4. + * + * @param string $baseCell Base cell + * + * @return string Cell range address + */ + protected static function readBIFF8CellRangeAddressB(string $subData, string $baseCell = 'A1'): string + { + [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell); + $baseCol = $baseCol - 1; + + // TODO: if cell range is just a single cell, should this function + // not just return e.g. 'A1' and not 'A1:A1' ? + + // offset: 0; size: 2; first row + $frIndex = self::getUInt2d($subData, 0); // adjust below + + // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767) + $lrIndex = self::getUInt2d($subData, 2); // adjust below + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 4))) { + // absolute column index + // offset: 4; size: 2; first column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $fcIndex = 0x00FF & self::getUInt2d($subData, 4); + $fc = Coordinate::stringFromColumnIndex($fcIndex + 1); + $fc = '$' . $fc; + } else { + // column offset + // offset: 4; size: 2; first column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4); + $fcIndex = $baseCol + $relativeFcIndex; + $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256; + $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256; + $fc = Coordinate::stringFromColumnIndex($fcIndex + 1); + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 4))) { + // absolute row index + $fr = $frIndex + 1; + $fr = '$' . $fr; + } else { + // row offset + $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536; + $fr = $baseRow + $frIndex; + } + + // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index) + if (!(0x4000 & self::getUInt2d($subData, 6))) { + // absolute column index + // offset: 6; size: 2; last column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $lcIndex = 0x00FF & self::getUInt2d($subData, 6); + $lc = Coordinate::stringFromColumnIndex($lcIndex + 1); + $lc = '$' . $lc; + } else { + // column offset + // offset: 6; size: 2; last column with relative/absolute flags + // bit: 7-0; mask 0x00FF; column index + $relativeLcIndex = 0x00FF & self::getInt2d($subData, 6); + $lcIndex = $baseCol + $relativeLcIndex; + $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256; + $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256; + $lc = Coordinate::stringFromColumnIndex($lcIndex + 1); + } + + // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index) + if (!(0x8000 & self::getUInt2d($subData, 6))) { + // absolute row index + $lr = $lrIndex + 1; + $lr = '$' . $lr; + } else { + // row offset + $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536; + $lr = $baseRow + $lrIndex; + } + + return "$fc$fr:$lc$lr"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color.php new file mode 100644 index 0000000..adbe1a2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color.php @@ -0,0 +1,29 @@ + 'FF0000'] + */ + public static function map(int $color, array $palette, int $version): array + { + if ($color <= 0x07 || $color >= 0x40) { + // special built-in color + return Color\BuiltIn::lookup($color); + } elseif (isset($palette[$color - 8])) { + // palette color, color index 0x08 maps to pallete index 0 + return $palette[$color - 8]; + } + + return ($version === Xls::XLS_BIFF8) ? Color\BIFF8::lookup($color) : Color\BIFF5::lookup($color); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php new file mode 100644 index 0000000..ecc026c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php @@ -0,0 +1,75 @@ + '000000', + 0x09 => 'FFFFFF', + 0x0A => 'FF0000', + 0x0B => '00FF00', + 0x0C => '0000FF', + 0x0D => 'FFFF00', + 0x0E => 'FF00FF', + 0x0F => '00FFFF', + 0x10 => '800000', + 0x11 => '008000', + 0x12 => '000080', + 0x13 => '808000', + 0x14 => '800080', + 0x15 => '008080', + 0x16 => 'C0C0C0', + 0x17 => '808080', + 0x18 => '8080FF', + 0x19 => '802060', + 0x1A => 'FFFFC0', + 0x1B => 'A0E0F0', + 0x1C => '600080', + 0x1D => 'FF8080', + 0x1E => '0080C0', + 0x1F => 'C0C0FF', + 0x20 => '000080', + 0x21 => 'FF00FF', + 0x22 => 'FFFF00', + 0x23 => '00FFFF', + 0x24 => '800080', + 0x25 => '800000', + 0x26 => '008080', + 0x27 => '0000FF', + 0x28 => '00CFFF', + 0x29 => '69FFFF', + 0x2A => 'E0FFE0', + 0x2B => 'FFFF80', + 0x2C => 'A6CAF0', + 0x2D => 'DD9CB3', + 0x2E => 'B38FEE', + 0x2F => 'E3E3E3', + 0x30 => '2A6FF9', + 0x31 => '3FB8CD', + 0x32 => '488436', + 0x33 => '958C41', + 0x34 => '8E5E42', + 0x35 => 'A0627A', + 0x36 => '624FAC', + 0x37 => '969696', + 0x38 => '1D2FBE', + 0x39 => '286676', + 0x3A => '004500', + 0x3B => '453E01', + 0x3C => '6A2813', + 0x3D => '85396A', + 0x3E => '4A3285', + 0x3F => '424242', + ]; + + /** + * Map color array from BIFF5 built-in color index. + * + * @return array{rgb: string} + */ + public static function lookup(int $color): array + { + return ['rgb' => self::BIFF5_COLOR_MAP[$color] ?? '000000']; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php new file mode 100644 index 0000000..11745f7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php @@ -0,0 +1,75 @@ + '000000', + 0x09 => 'FFFFFF', + 0x0A => 'FF0000', + 0x0B => '00FF00', + 0x0C => '0000FF', + 0x0D => 'FFFF00', + 0x0E => 'FF00FF', + 0x0F => '00FFFF', + 0x10 => '800000', + 0x11 => '008000', + 0x12 => '000080', + 0x13 => '808000', + 0x14 => '800080', + 0x15 => '008080', + 0x16 => 'C0C0C0', + 0x17 => '808080', + 0x18 => '9999FF', + 0x19 => '993366', + 0x1A => 'FFFFCC', + 0x1B => 'CCFFFF', + 0x1C => '660066', + 0x1D => 'FF8080', + 0x1E => '0066CC', + 0x1F => 'CCCCFF', + 0x20 => '000080', + 0x21 => 'FF00FF', + 0x22 => 'FFFF00', + 0x23 => '00FFFF', + 0x24 => '800080', + 0x25 => '800000', + 0x26 => '008080', + 0x27 => '0000FF', + 0x28 => '00CCFF', + 0x29 => 'CCFFFF', + 0x2A => 'CCFFCC', + 0x2B => 'FFFF99', + 0x2C => '99CCFF', + 0x2D => 'FF99CC', + 0x2E => 'CC99FF', + 0x2F => 'FFCC99', + 0x30 => '3366FF', + 0x31 => '33CCCC', + 0x32 => '99CC00', + 0x33 => 'FFCC00', + 0x34 => 'FF9900', + 0x35 => 'FF6600', + 0x36 => '666699', + 0x37 => '969696', + 0x38 => '003366', + 0x39 => '339966', + 0x3A => '003300', + 0x3B => '333300', + 0x3C => '993300', + 0x3D => '993366', + 0x3E => '333399', + 0x3F => '333333', + ]; + + /** + * Map color array from BIFF8 built-in color index. + * + * @return array{rgb: string} + */ + public static function lookup(int $color): array + { + return ['rgb' => self::BIFF8_COLOR_MAP[$color] ?? '000000']; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php new file mode 100644 index 0000000..5752446 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php @@ -0,0 +1,31 @@ + '000000', + 0x01 => 'FFFFFF', + 0x02 => 'FF0000', + 0x03 => '00FF00', + 0x04 => '0000FF', + 0x05 => 'FFFF00', + 0x06 => 'FF00FF', + 0x07 => '00FFFF', + 0x40 => '000000', // system window text color + 0x41 => 'FFFFFF', // system window background color + ]; + + /** + * Map built-in color to RGB value. + * + * @param int $color Indexed color + * + * @return array{rgb: string} + */ + public static function lookup(int $color): array + { + return ['rgb' => self::BUILTIN_COLOR_MAP[$color] ?? '000000']; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php new file mode 100644 index 0000000..9f89d16 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php @@ -0,0 +1,345 @@ + + */ + private static array $types = [ + 0x01 => Conditional::CONDITION_CELLIS, + 0x02 => Conditional::CONDITION_EXPRESSION, + ]; + + /** + * @var array + */ + private static array $operators = [ + 0x00 => Conditional::OPERATOR_NONE, + 0x01 => Conditional::OPERATOR_BETWEEN, + 0x02 => Conditional::OPERATOR_NOTBETWEEN, + 0x03 => Conditional::OPERATOR_EQUAL, + 0x04 => Conditional::OPERATOR_NOTEQUAL, + 0x05 => Conditional::OPERATOR_GREATERTHAN, + 0x06 => Conditional::OPERATOR_LESSTHAN, + 0x07 => Conditional::OPERATOR_GREATERTHANOREQUAL, + 0x08 => Conditional::OPERATOR_LESSTHANOREQUAL, + ]; + + public static function type(int $type): ?string + { + return self::$types[$type] ?? null; + } + + public static function operator(int $operator): ?string + { + return self::$operators[$operator] ?? null; + } + + /** + * Parse conditional formatting blocks. + * + * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE + * + * @return mixed[] + */ + protected function readCFHeader2(Xls $xls): array + { + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer forward to next record + $xls->pos += 4 + $length; + + if ($xls->readDataOnly) { + return []; + } + + // offset: 0; size: 2; Rule Count +// $ruleCount = self::getUInt2d($recordData, 0); + + // offset: var; size: var; cell range address list with + $cellRangeAddressList = ($xls->version == self::XLS_BIFF8) + ? Biff8::readBIFF8CellRangeAddressList(substr($recordData, 12)) + : Biff5::readBIFF5CellRangeAddressList(substr($recordData, 12)); + $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; + + return $cellRangeAddresses; + } + + /** @param string[] $cellRangeAddresses */ + protected function readCFRule2(array $cellRangeAddresses, Xls $xls): void + { + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer forward to next record + $xls->pos += 4 + $length; + + if ($xls->readDataOnly) { + return; + } + + // offset: 0; size: 2; Options + $cfRule = self::getUInt2d($recordData, 0); + + // bit: 8-15; mask: 0x00FF; type + $type = (0x00FF & $cfRule) >> 0; + $type = self::type($type); + + // bit: 0-7; mask: 0xFF00; type + $operator = (0xFF00 & $cfRule) >> 8; + $operator = self::operator($operator); + + if ($type === null || $operator === null) { + return; + } + + // offset: 2; size: 2; Size1 + $size1 = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; Size2 + $size2 = self::getUInt2d($recordData, 4); + + // offset: 6; size: 4; Options + $options = self::getInt4d($recordData, 6); + + $style = new Style(false, true); // non-supervisor, conditional + $noFormatSet = true; + //$xls->getCFStyleOptions($options, $style); + + $hasFontRecord = (bool) ((0x04000000 & $options) >> 26); + $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27); + $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28); + $hasFillRecord = (bool) ((0x20000000 & $options) >> 29); + $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30); + // note unexpected values for following 4 + $hasBorderLeft = !(bool) (0x00000400 & $options); + $hasBorderRight = !(bool) (0x00000800 & $options); + $hasBorderTop = !(bool) (0x00001000 & $options); + $hasBorderBottom = !(bool) (0x00002000 & $options); + + $offset = 12; + + if ($hasFontRecord === true) { + $fontStyle = substr($recordData, $offset, 118); + $this->getCFFontStyle($fontStyle, $style, $xls); + $offset += 118; + $noFormatSet = false; + } + + if ($hasAlignmentRecord === true) { + //$alignmentStyle = substr($recordData, $offset, 8); + //$this->getCFAlignmentStyle($alignmentStyle, $style, $xls); + $offset += 8; + } + + if ($hasBorderRecord === true) { + $borderStyle = substr($recordData, $offset, 8); + $this->getCFBorderStyle($borderStyle, $style, $hasBorderLeft, $hasBorderRight, $hasBorderTop, $hasBorderBottom, $xls); + $offset += 8; + $noFormatSet = false; + } + + if ($hasFillRecord === true) { + $fillStyle = substr($recordData, $offset, 4); + $this->getCFFillStyle($fillStyle, $style, $xls); + $offset += 4; + $noFormatSet = false; + } + + if ($hasProtectionRecord === true) { + //$protectionStyle = substr($recordData, $offset, 4); + //$this->getCFProtectionStyle($protectionStyle, $style, $xls); + $offset += 2; + } + + $formula1 = $formula2 = null; + if ($size1 > 0) { + $formula1 = $this->readCFFormula($recordData, $offset, $size1, $xls); + if ($formula1 === null) { + return; + } + + $offset += $size1; + } + + if ($size2 > 0) { + $formula2 = $this->readCFFormula($recordData, $offset, $size2, $xls); + if ($formula2 === null) { + return; + } + + $offset += $size2; + } + + $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style, $noFormatSet, $xls); + } + + /*private function getCFStyleOptions(int $options, Style $style, Xls $xls): void + { + }*/ + + private function getCFFontStyle(string $options, Style $style, Xls $xls): void + { + $fontSize = self::getInt4d($options, 64); + if ($fontSize !== -1) { + $style->getFont()->setSize($fontSize / 20); // Convert twips to points + } + $options68 = self::getInt4d($options, 68); + $options88 = self::getInt4d($options, 88); + + if (($options88 & 2) === 0) { + $bold = self::getUInt2d($options, 72); // 400 = normal, 700 = bold + if ($bold !== 0) { + $style->getFont()->setBold($bold >= 550); + } + if (($options68 & 2) !== 0) { + $style->getFont()->setItalic(true); + } + } + if (($options88 & 0x80) === 0) { + if (($options68 & 0x80) !== 0) { + $style->getFont()->setStrikethrough(true); + } + } + + $color = self::getInt4d($options, 80); + + if ($color !== -1) { + $style->getFont() + ->getColor() + ->setRGB(Color::map($color, $xls->palette, $xls->version)['rgb']); + } + } + + /*private function getCFAlignmentStyle(string $options, Style $style, Xls $xls): void + { + }*/ + + private function getCFBorderStyle(string $options, Style $style, bool $hasBorderLeft, bool $hasBorderRight, bool $hasBorderTop, bool $hasBorderBottom, Xls $xls): void + { + /** @var false|int[] */ + $valueArray = unpack('V', $options); + $value = is_array($valueArray) ? $valueArray[1] : 0; + $left = $value & 15; + $right = ($value >> 4) & 15; + $top = ($value >> 8) & 15; + $bottom = ($value >> 12) & 15; + $leftc = ($value >> 16) & 0x7F; + $rightc = ($value >> 23) & 0x7F; + /** @var false|int[] */ + $valueArray = unpack('V', substr($options, 4)); + $value = is_array($valueArray) ? $valueArray[1] : 0; + $topc = $value & 0x7F; + $bottomc = ($value & 0x3F80) >> 7; + if ($hasBorderLeft) { + $style->getBorders()->getLeft() + ->setBorderStyle(self::BORDER_STYLE_MAP[$left]); + $style->getBorders()->getLeft()->getColor() + ->setRGB(Color::map($leftc, $xls->palette, $xls->version)['rgb']); + } + if ($hasBorderRight) { + $style->getBorders()->getRight() + ->setBorderStyle(self::BORDER_STYLE_MAP[$right]); + $style->getBorders()->getRight()->getColor() + ->setRGB(Color::map($rightc, $xls->palette, $xls->version)['rgb']); + } + if ($hasBorderTop) { + $style->getBorders()->getTop() + ->setBorderStyle(self::BORDER_STYLE_MAP[$top]); + $style->getBorders()->getTop()->getColor() + ->setRGB(Color::map($topc, $xls->palette, $xls->version)['rgb']); + } + if ($hasBorderBottom) { + $style->getBorders()->getBottom() + ->setBorderStyle(self::BORDER_STYLE_MAP[$bottom]); + $style->getBorders()->getBottom()->getColor() + ->setRGB(Color::map($bottomc, $xls->palette, $xls->version)['rgb']); + } + } + + private function getCFFillStyle(string $options, Style $style, Xls $xls): void + { + $fillPattern = self::getUInt2d($options, 0); + // bit: 10-15; mask: 0xFC00; type + $fillPattern = (0xFC00 & $fillPattern) >> 10; + $fillPattern = FillPattern::lookup($fillPattern); + $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern; + + if ($fillPattern !== Fill::FILL_NONE) { + $style->getFill()->setFillType($fillPattern); + + $fillColors = self::getUInt2d($options, 2); + + // bit: 0-6; mask: 0x007F; type + $color1 = (0x007F & $fillColors) >> 0; + + // bit: 7-13; mask: 0x3F80; type + $color2 = (0x3F80 & $fillColors) >> 7; + if ($fillPattern === Fill::FILL_SOLID) { + $style->getFill()->getStartColor()->setRGB(Color::map($color2, $xls->palette, $xls->version)['rgb']); + } else { + $style->getFill()->getStartColor()->setRGB(Color::map($color1, $xls->palette, $xls->version)['rgb']); + $style->getFill()->getEndColor()->setRGB(Color::map($color2, $xls->palette, $xls->version)['rgb']); + } + } + } + + /*private function getCFProtectionStyle(string $options, Style $style, Xls $xls): void + { + }*/ + + private function readCFFormula(string $recordData, int $offset, int $size, Xls $xls): float|int|string|null + { + try { + $formula = substr($recordData, $offset, $size); + $formula = pack('v', $size) . $formula; // prepend the length + + $formula = $xls->getFormulaFromStructure($formula); + if (is_numeric($formula)) { + return (str_contains($formula, '.')) ? (float) $formula : (int) $formula; + } + + return $formula; + } catch (PhpSpreadsheetException) { + return null; + } + } + + /** @param string[] $cellRanges */ + private function setCFRules(array $cellRanges, string $type, string $operator, null|float|int|string $formula1, null|float|int|string $formula2, Style $style, bool $noFormatSet, Xls $xls): void + { + foreach ($cellRanges as $cellRange) { + $conditional = new Conditional(); + $conditional->setNoFormatSet($noFormatSet); + $conditional->setConditionType($type); + $conditional->setOperatorType($operator); + $conditional->setStopIfTrue(true); + if ($formula1 !== null) { + $conditional->addCondition($formula1); + } + if ($formula2 !== null) { + $conditional->addCondition($formula2); + } + $conditional->setStyle($style); + + $conditionalStyles = $xls->phpSheet + ->getStyle($cellRange) + ->getConditionalStyles(); + $conditionalStyles[] = $conditional; + + $xls->phpSheet + ->getStyle($cellRange) + ->setConditionalStyles($conditionalStyles); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php new file mode 100644 index 0000000..b5a9454 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php @@ -0,0 +1,219 @@ + + */ + private static array $types = [ + 0x00 => DataValidation::TYPE_NONE, + 0x01 => DataValidation::TYPE_WHOLE, + 0x02 => DataValidation::TYPE_DECIMAL, + 0x03 => DataValidation::TYPE_LIST, + 0x04 => DataValidation::TYPE_DATE, + 0x05 => DataValidation::TYPE_TIME, + 0x06 => DataValidation::TYPE_TEXTLENGTH, + 0x07 => DataValidation::TYPE_CUSTOM, + ]; + + /** + * @var array + */ + private static array $errorStyles = [ + 0x00 => DataValidation::STYLE_STOP, + 0x01 => DataValidation::STYLE_WARNING, + 0x02 => DataValidation::STYLE_INFORMATION, + ]; + + /** + * @var array + */ + private static array $operators = [ + 0x00 => DataValidation::OPERATOR_BETWEEN, + 0x01 => DataValidation::OPERATOR_NOTBETWEEN, + 0x02 => DataValidation::OPERATOR_EQUAL, + 0x03 => DataValidation::OPERATOR_NOTEQUAL, + 0x04 => DataValidation::OPERATOR_GREATERTHAN, + 0x05 => DataValidation::OPERATOR_LESSTHAN, + 0x06 => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 0x07 => DataValidation::OPERATOR_LESSTHANOREQUAL, + ]; + + public static function type(int $type): ?string + { + return self::$types[$type] ?? null; + } + + public static function errorStyle(int $errorStyle): ?string + { + return self::$errorStyles[$errorStyle] ?? null; + } + + public static function operator(int $operator): ?string + { + return self::$operators[$operator] ?? null; + } + + /** + * Read DATAVALIDATION record. + */ + protected function readDataValidation2(Xls $xls): void + { + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer forward to next record + $xls->pos += 4 + $length; + + if ($xls->readDataOnly) { + return; + } + + // offset: 0; size: 4; Options + $options = self::getInt4d($recordData, 0); + + // bit: 0-3; mask: 0x0000000F; type + $type = (0x0000000F & $options) >> 0; + $type = self::type($type); + + // bit: 4-6; mask: 0x00000070; error type + $errorStyle = (0x00000070 & $options) >> 4; + $errorStyle = self::errorStyle($errorStyle); + + // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list) + // I have only seen cases where this is 1 + //$explicitFormula = (0x00000080 & $options) >> 7; + + // bit: 8; mask: 0x00000100; 1= empty cells allowed + $allowBlank = (0x00000100 & $options) >> 8; + + // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity + $suppressDropDown = (0x00000200 & $options) >> 9; + + // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected + $showInputMessage = (0x00040000 & $options) >> 18; + + // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered + $showErrorMessage = (0x00080000 & $options) >> 19; + + // bit: 20-23; mask: 0x00F00000; condition operator + $operator = (0x00F00000 & $options) >> 20; + $operator = self::operator($operator); + + if ($type === null || $errorStyle === null || $operator === null) { + return; + } + + // offset: 4; size: var; title of the prompt box + $offset = 4; + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $promptTitle = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: var; title of the error box + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $errorTitle = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: var; text of the prompt box + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $prompt = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: var; text of the error box + $string = self::readUnicodeStringLong(substr($recordData, $offset)); + $error = $string['value'] !== chr(0) ? $string['value'] : ''; + $offset += $string['size']; + + // offset: var; size: 2; size of the formula data for the first condition + $sz1 = self::getUInt2d($recordData, $offset); + $offset += 2; + + // offset: var; size: 2; not used + $offset += 2; + + // offset: var; size: $sz1; formula data for first condition (without size field) + $formula1 = substr($recordData, $offset, $sz1); + $formula1 = pack('v', $sz1) . $formula1; // prepend the length + + try { + $formula1 = $xls->getFormulaFromStructure($formula1); + + // in list type validity, null characters are used as item separators + if ($type == DataValidation::TYPE_LIST) { + $formula1 = str_replace(chr(0), ',', $formula1); + } + } catch (PhpSpreadsheetException $e) { + return; + } + $offset += $sz1; + + // offset: var; size: 2; size of the formula data for the first condition + $sz2 = self::getUInt2d($recordData, $offset); + $offset += 2; + + // offset: var; size: 2; not used + $offset += 2; + + // offset: var; size: $sz2; formula data for second condition (without size field) + $formula2 = substr($recordData, $offset, $sz2); + $formula2 = pack('v', $sz2) . $formula2; // prepend the length + + try { + $formula2 = $xls->getFormulaFromStructure($formula2); + } catch (PhpSpreadsheetException) { + return; + } + $offset += $sz2; + + // offset: var; size: var; cell range address list with + $cellRangeAddressList = Biff8::readBIFF8CellRangeAddressList(substr($recordData, $offset)); + /** @var string[] */ + $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; + $maxRow = (string) AddressRange::MAX_ROW; + $maxCol = AddressRange::MAX_COLUMN; + $maxXlsRow = (string) XlsWorksheet::MAX_XLS_ROW; + $maxXlsColumnString = (string) XlsWorksheet::MAX_XLS_COLUMN_STRING; + + foreach ($cellRangeAddresses as $cellRange) { + $cellRange = preg_replace( + [ + "/([a-z]+)1:([a-z]+)$maxXlsRow/i", + "/([a-z]+\\d+):([a-z]+)$maxXlsRow/i", + "/A(\\d+):$maxXlsColumnString(\\d+)/i", + "/([a-z]+\\d+):$maxXlsColumnString(\\d+)/i", + ], + [ + '$1:$2', + '$1:${2}' . $maxRow, + '$1:$2', + '$1:' . $maxCol . '$2', + ], + $cellRange + ) ?? $cellRange; + $objValidation = new DataValidation(); + $objValidation->setType($type); + $objValidation->setErrorStyle($errorStyle); + $objValidation->setAllowBlank((bool) $allowBlank); + $objValidation->setShowInputMessage((bool) $showInputMessage); + $objValidation->setShowErrorMessage((bool) $showErrorMessage); + $objValidation->setShowDropDown(!$suppressDropDown); + $objValidation->setOperator($operator); + $objValidation->setErrorTitle($errorTitle); + $objValidation->setError($error); + $objValidation->setPromptTitle($promptTitle); + $objValidation->setPrompt($prompt); + $objValidation->setFormula1($formula1); + $objValidation->setFormula2($formula2); + $xls->phpSheet->setDataValidation($cellRange, $objValidation); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php new file mode 100644 index 0000000..fa8f8cd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php @@ -0,0 +1,24 @@ + '#NULL!', + 0x07 => '#DIV/0!', + 0x0F => '#VALUE!', + 0x17 => '#REF!', + 0x1D => '#NAME?', + 0x24 => '#NUM!', + 0x2A => '#N/A', + ]; + + /** + * Map error code, e.g. '#N/A'. + */ + public static function lookup(int $code): string|bool + { + return self::ERROR_CODE_MAP[$code] ?? false; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Escher.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Escher.php new file mode 100644 index 0000000..018c171 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Escher.php @@ -0,0 +1,616 @@ +object = $object; + } + + private const WHICH_ROUTINE = [ + self::DGGCONTAINER => 'readDggContainer', + self::DGG => 'readDgg', + self::BSTORECONTAINER => 'readBstoreContainer', + self::BSE => 'readBSE', + self::BLIPJPEG => 'readBlipJPEG', + self::BLIPPNG => 'readBlipPNG', + self::OPT => 'readOPT', + self::TERTIARYOPT => 'readTertiaryOPT', + self::SPLITMENUCOLORS => 'readSplitMenuColors', + self::DGCONTAINER => 'readDgContainer', + self::DG => 'readDg', + self::SPGRCONTAINER => 'readSpgrContainer', + self::SPCONTAINER => 'readSpContainer', + self::SPGR => 'readSpgr', + self::SP => 'readSp', + self::CLIENTTEXTBOX => 'readClientTextbox', + self::CLIENTANCHOR => 'readClientAnchor', + self::CLIENTDATA => 'readClientData', + ]; + + /** + * Load Escher stream data. May be a partial Escher stream. + * + * @return T + */ + public function load(string $data): BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer + { + $this->data = $data; + + // total byte size of Excel data (workbook global substream + sheet substreams) + $this->dataSize = strlen($this->data); + + $this->pos = 0; + + // Parse Escher stream + while ($this->pos < $this->dataSize) { + // offset: 2; size: 2: Record Type + $fbt = Xls::getUInt2d($this->data, $this->pos + 2); + $routine = self::WHICH_ROUTINE[$fbt] ?? 'readDefault'; + if (method_exists($this, $routine)) { + $this->$routine(); + } + } + + return $this->object; + } + + /** + * Read a generic record. + */ + private function readDefault(): void + { + // offset 0; size: 2; recVer and recInstance + //$verInstance = Xls::getUInt2d($this->data, $this->pos); + + // offset: 2; size: 2: Record Type + //$fbt = Xls::getUInt2d($this->data, $this->pos + 2); + + // bit: 0-3; mask: 0x000F; recVer + //$recVer = (0x000F & $verInstance) >> 0; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read DggContainer record (Drawing Group Container). + */ + private function readDggContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $dggContainer = new DggContainer(); + $this->applyAttribute('setDggContainer', $dggContainer); + $reader = new self($dggContainer); + $reader->load($recordData); + } + + /** + * Read Dgg record (Drawing Group). + */ + private function readDgg(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read BstoreContainer record (Blip Store Container). + */ + private function readBstoreContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $bstoreContainer = new BstoreContainer(); + $this->applyAttribute('setBstoreContainer', $bstoreContainer); + $reader = new self($bstoreContainer); + $reader->load($recordData); + } + + /** + * Read BSE record. + */ + private function readBSE(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // add BSE to BstoreContainer + $BSE = new BSE(); + $this->applyAttribute('addBSE', $BSE); + + $BSE->setBLIPType($recInstance); + + // offset: 0; size: 1; btWin32 (MSOBLIPTYPE) + //$btWin32 = ord($recordData[0]); + + // offset: 1; size: 1; btWin32 (MSOBLIPTYPE) + //$btMacOS = ord($recordData[1]); + + // offset: 2; size: 16; MD4 digest + //$rgbUid = substr($recordData, 2, 16); + + // offset: 18; size: 2; tag + //$tag = Xls::getUInt2d($recordData, 18); + + // offset: 20; size: 4; size of BLIP in bytes + //$size = Xls::getInt4d($recordData, 20); + + // offset: 24; size: 4; number of references to this BLIP + //$cRef = Xls::getInt4d($recordData, 24); + + // offset: 28; size: 4; MSOFO file offset + //$foDelay = Xls::getInt4d($recordData, 28); + + // offset: 32; size: 1; unused1 + //$unused1 = ord($recordData[32]); + + // offset: 33; size: 1; size of nameData in bytes (including null terminator) + $cbName = ord($recordData[33]); + + // offset: 34; size: 1; unused2 + //$unused2 = ord($recordData[34]); + + // offset: 35; size: 1; unused3 + //$unused3 = ord($recordData[35]); + + // offset: 36; size: $cbName; nameData + //$nameData = substr($recordData, 36, $cbName); + + // offset: 36 + $cbName, size: var; the BLIP data + $blipData = substr($recordData, 36 + $cbName); + + // record is a container, read contents + $reader = new self($BSE); + $reader->load($blipData); + } + + /** + * Read BlipJPEG record. Holds raw JPEG image data. + */ + private function readBlipJPEG(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $pos = 0; + + // offset: 0; size: 16; rgbUid1 (MD4 digest of) + //$rgbUid1 = substr($recordData, 0, 16); + $pos += 16; + + // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 + if (in_array($recInstance, [0x046B, 0x06E3])) { + //$rgbUid2 = substr($recordData, 16, 16); + $pos += 16; + } + + // offset: var; size: 1; tag + //$tag = ord($recordData[$pos]); + ++$pos; + + // offset: var; size: var; the raw image data + $data = substr($recordData, $pos); + + $blip = new Blip(); + $blip->setData($data); + + $this->applyAttribute('setBlip', $blip); + } + + /** + * Read BlipPNG record. Holds raw PNG image data. + */ + private function readBlipPNG(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $pos = 0; + + // offset: 0; size: 16; rgbUid1 (MD4 digest of) + //$rgbUid1 = substr($recordData, 0, 16); + $pos += 16; + + // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3 + if ($recInstance == 0x06E1) { + //$rgbUid2 = substr($recordData, 16, 16); + $pos += 16; + } + + // offset: var; size: 1; tag + //$tag = ord($recordData[$pos]); + ++$pos; + + // offset: var; size: var; the raw image data + $data = substr($recordData, $pos); + + $blip = new Blip(); + $blip->setData($data); + + $this->applyAttribute('setBlip', $blip); + } + + /** + * Read OPT record. This record may occur within DggContainer record or SpContainer. + */ + private function readOPT(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + $this->readOfficeArtRGFOPTE($recordData, $recInstance); + } + + /** + * Read TertiaryOPT record. + */ + private function readTertiaryOPT(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + //$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read SplitMenuColors record. + */ + private function readSplitMenuColors(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read DgContainer record (Drawing Container). + */ + private function readDgContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $dgContainer = new DgContainer(); + $this->applyAttribute('setDgContainer', $dgContainer); + $reader = new self($dgContainer); + $reader->load($recordData); + } + + /** + * Read Dg record (Drawing). + */ + private function readDg(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read SpgrContainer record (Shape Group Container). + */ + private function readSpgrContainer(): void + { + // context is either context DgContainer or SpgrContainer + + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $spgrContainer = new SpgrContainer(); + + if ($this->object instanceof DgContainer) { + // DgContainer + $this->object->setSpgrContainer($spgrContainer); + } elseif ($this->object instanceof SpgrContainer) { + // SpgrContainer + $this->object->addChild($spgrContainer); + } + + $reader = new self($spgrContainer); + $reader->load($recordData); + } + + /** + * Read SpContainer record (Shape Container). + */ + private function readSpContainer(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // add spContainer to spgrContainer + $spContainer = new SpContainer(); + $this->applyAttribute('addChild', $spContainer); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // record is a container, read contents + $reader = new self($spContainer); + $reader->load($recordData); + } + + /** + * Read Spgr record (Shape Group). + */ + private function readSpgr(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read Sp record (Shape). + */ + private function readSp(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + //$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read ClientTextbox record. + */ + private function readClientTextbox(): void + { + // offset: 0; size: 2; recVer and recInstance + + // bit: 4-15; mask: 0xFFF0; recInstance + //$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4; + + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read ClientAnchor record. This record holds information about where the shape is anchored in worksheet. + */ + private function readClientAnchor(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + $recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + + // offset: 2; size: 2; upper-left corner column index (0-based) + $c1 = Xls::getUInt2d($recordData, 2); + + // offset: 4; size: 2; upper-left corner horizontal offset in 1/1024 of column width + $startOffsetX = Xls::getUInt2d($recordData, 4); + + // offset: 6; size: 2; upper-left corner row index (0-based) + $r1 = Xls::getUInt2d($recordData, 6); + + // offset: 8; size: 2; upper-left corner vertical offset in 1/256 of row height + $startOffsetY = Xls::getUInt2d($recordData, 8); + + // offset: 10; size: 2; bottom-right corner column index (0-based) + $c2 = Xls::getUInt2d($recordData, 10); + + // offset: 12; size: 2; bottom-right corner horizontal offset in 1/1024 of column width + $endOffsetX = Xls::getUInt2d($recordData, 12); + + // offset: 14; size: 2; bottom-right corner row index (0-based) + $r2 = Xls::getUInt2d($recordData, 14); + + // offset: 16; size: 2; bottom-right corner vertical offset in 1/256 of row height + $endOffsetY = Xls::getUInt2d($recordData, 16); + + $this->applyAttribute('setStartCoordinates', Coordinate::stringFromColumnIndex($c1 + 1) . ($r1 + 1)); + $this->applyAttribute('setStartOffsetX', $startOffsetX); + $this->applyAttribute('setStartOffsetY', $startOffsetY); + $this->applyAttribute('setEndCoordinates', Coordinate::stringFromColumnIndex($c2 + 1) . ($r2 + 1)); + $this->applyAttribute('setEndOffsetX', $endOffsetX); + $this->applyAttribute('setEndOffsetY', $endOffsetY); + } + + private function applyAttribute(string $name, mixed $value): void + { + if (method_exists($this->object, $name)) { + $this->object->$name($value); + } + } + + /** + * Read ClientData record. + */ + private function readClientData(): void + { + $length = Xls::getInt4d($this->data, $this->pos + 4); + //$recordData = substr($this->data, $this->pos + 8, $length); + + // move stream pointer to next record + $this->pos += 8 + $length; + } + + /** + * Read OfficeArtRGFOPTE table of property-value pairs. + * + * @param string $data Binary data + * @param int $n Number of properties + */ + private function readOfficeArtRGFOPTE(string $data, int $n): void + { + $splicedComplexData = substr($data, 6 * $n); + + // loop through property-value pairs + for ($i = 0; $i < $n; ++$i) { + // read 6 bytes at a time + $fopte = substr($data, 6 * $i, 6); + + // offset: 0; size: 2; opid + $opid = Xls::getUInt2d($fopte, 0); + + // bit: 0-13; mask: 0x3FFF; opid.opid + $opidOpid = (0x3FFF & $opid) >> 0; + + // bit: 14; mask 0x4000; 1 = value in op field is BLIP identifier + //$opidFBid = (0x4000 & $opid) >> 14; + + // bit: 15; mask 0x8000; 1 = this is a complex property, op field specifies size of complex data + $opidFComplex = (0x8000 & $opid) >> 15; + + // offset: 2; size: 4; the value for this property + $op = Xls::getInt4d($fopte, 2); + + if ($opidFComplex) { + $complexData = substr($splicedComplexData, 0, $op); + $splicedComplexData = substr($splicedComplexData, $op); + + // we store string value with complex data + $value = $complexData; + } else { + // we store integer value + $value = $op; + } + + if (method_exists($this->object, 'setOPT')) { + $this->object->setOPT($opidOpid, $value); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php new file mode 100644 index 0000000..9856989 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php @@ -0,0 +1,267 @@ +loadOLE($filename); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + $xls->pos = 0; + $xls->sheets = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EOF => $xls->readDefault(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + foreach ($xls->sheets as $sheet) { + if ($sheet['sheetType'] === 0x00) { + // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module + $worksheetNames[] = $sheet['name']; + } + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + protected function listWorksheetInfo2(string $filename, Xls $xls): array + { + File::assertFile($filename); + + $worksheetInfo = []; + + // Read the OLE file + $xls->loadOLE($filename); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + // initialize + $xls->pos = 0; + $xls->sheets = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EOF => $xls->readDefault(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + // Parse the individual sheets + foreach ($xls->sheets as $sheet) { + if ($sheet['sheetType'] !== 0x00) { + // 0x00: Worksheet + // 0x02: Chart + // 0x06: Visual Basic module + continue; + } + + $tmpInfo = []; + $tmpInfo['worksheetName'] = StringHelper::convertToString($sheet['name']); + $tmpInfo['lastColumnLetter'] = 'A'; + $tmpInfo['lastColumnIndex'] = 0; + $tmpInfo['totalRows'] = 0; + $tmpInfo['totalColumns'] = 0; + $tmpInfo['sheetState'] = StringHelper::convertToString($sheet['sheetState']); + + $xls->pos = $sheet['offset']; + + while ($xls->pos <= $xls->dataSize - 4) { + $code = self::getUInt2d($xls->data, $xls->pos); + + switch ($code) { + case self::XLS_TYPE_RK: + case self::XLS_TYPE_LABELSST: + case self::XLS_TYPE_NUMBER: + case self::XLS_TYPE_FORMULA: + case self::XLS_TYPE_BOOLERR: + case self::XLS_TYPE_LABEL: + case self::XLS_TYPE_MULRK: + $length = self::getUInt2d($xls->data, $xls->pos + 2); + $recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length); + + // move stream pointer to next record + $xls->pos += 4 + $length; + + $rowIndex = self::getUInt2d($recordData, 0) + 1; + if ($code === self::XLS_TYPE_MULRK) { + $columnIndex = self::getUInt2d($recordData, $length - 2); + } else { + $columnIndex = self::getUInt2d($recordData, 2); + } + + $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); + $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); + + break; + case self::XLS_TYPE_BOF: + $xls->readBof(); + + break; + case self::XLS_TYPE_EOF: + $xls->readDefault(); + + break 2; + default: + $xls->readDefault(); + + break; + } + } + + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; + + $worksheetInfo[] = $tmpInfo; + } + + return $worksheetInfo; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + protected function listWorksheetDimensions2(string $filename, Xls $xls): array + { + File::assertFile($filename); + + $worksheetInfo = []; + + // Read the OLE file + $xls->loadOLE($filename); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + // initialize + $xls->pos = 0; + $xls->sheets = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EOF => $xls->readDefault(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + // Parse the individual sheets + foreach ($xls->sheets as $sheet) { + if ($sheet['sheetType'] !== 0x00) { + // 0x00: Worksheet + // 0x02: Chart + // 0x06: Visual Basic module + continue; + } + + $tmpInfo = []; + $tmpInfo['worksheetName'] = StringHelper::convertToString($sheet['name']); + $tmpInfo['dimensionsMinR'] = -1; + $tmpInfo['dimensionsMaxR'] = -1; + $tmpInfo['dimensionsMinC'] = -1; + $tmpInfo['dimensionsMaxC'] = -1; + $tmpInfo['lastColumnLetter'] = ''; + + $xls->pos = $sheet['offset']; + + while ($xls->pos <= $xls->dataSize - 4) { + $code = self::getUInt2d($xls->data, $xls->pos); + + switch ($code) { + case self::XLS_TYPE_BOF: + $xls->readBof(); + + break; + case self::XLS_TYPE_EOF: + $xls->readDefault(); + + break 2; + case self::XLS_TYPE_DIMENSION: + $length = self::getUInt2d($xls->data, $xls->pos + 2); + if ($length === 14) { + $dimensionsData = substr($xls->data, $xls->pos + 4, $length); + $data = unpack('VrwMic/VrwMac/vcolMic/vcolMac/vreserved', $dimensionsData); + if (is_array($data)) { + /** @var int[] $data */ + $tmpInfo['dimensionsMinR'] = $data['rwMic']; + $tmpInfo['dimensionsMaxR'] = $data['rwMac']; + $tmpInfo['dimensionsMinC'] = $data['colMic']; + $tmpInfo['dimensionsMaxC'] = $data['colMac']; + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['dimensionsMaxC']); + } + } + $xls->readDefault(); + + break; + default: + $xls->readDefault(); + + break; + } + } + + $worksheetInfo[] = $tmpInfo; + } + + return $worksheetInfo; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php new file mode 100644 index 0000000..85adc7f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php @@ -0,0 +1,692 @@ +loadOLE($filename); + + // Initialisations + $xls->spreadsheet = $this->newSpreadsheet(); + $xls->spreadsheet->setValueBinder($xls->valueBinder); + $xls->spreadsheet->removeSheetByIndex(0); // remove 1st sheet + if (!$xls->readDataOnly) { + $xls->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style + $xls->spreadsheet->removeCellXfByIndex(0); // remove the default style + } + + // Read the summary information stream (containing metadata) + $xls->readSummaryInformation(); + + // Read the Additional document summary information stream (containing application-specific metadata) + $xls->readDocumentSummaryInformation(); + + // total byte size of Excel data (workbook global substream + sheet substreams) + $xls->dataSize = strlen($xls->data); + + // initialize + $xls->pos = 0; + $xls->codepage = $xls->codepage ?: CodePage::DEFAULT_CODE_PAGE; + $xls->formats = []; + $xls->objFonts = []; + $xls->palette = []; + $xls->sheets = []; + $xls->externalBooks = []; + $xls->ref = []; + $xls->definedname = []; //* @phpstan-ignore-line + $xls->sst = []; + $xls->drawingGroupData = ''; + $xls->xfIndex = 0; + $xls->mapCellXfIndex = []; + $xls->mapCellStyleXfIndex = []; + + // Parse Workbook Global Substream + while ($xls->pos < $xls->dataSize) { + $code = self::getUInt2d($xls->data, $xls->pos); + + match ($code) { + self::XLS_TYPE_BOF => $xls->readBof(), + self::XLS_TYPE_FILEPASS => $xls->readFilepass(), + self::XLS_TYPE_CODEPAGE => $xls->readCodepage(), + self::XLS_TYPE_DATEMODE => $xls->readDateMode(), + self::XLS_TYPE_FONT => $xls->readFont(), + self::XLS_TYPE_FORMAT => $xls->readFormat(), + self::XLS_TYPE_XF => $xls->readXf(), + self::XLS_TYPE_XFEXT => $xls->readXfExt(), + self::XLS_TYPE_STYLE => $xls->readStyle(), + self::XLS_TYPE_PALETTE => $xls->readPalette(), + self::XLS_TYPE_SHEET => $xls->readSheet(), + self::XLS_TYPE_EXTERNALBOOK => $xls->readExternalBook(), + self::XLS_TYPE_EXTERNNAME => $xls->readExternName(), + self::XLS_TYPE_EXTERNSHEET => $xls->readExternSheet(), + self::XLS_TYPE_DEFINEDNAME => $xls->readDefinedName(), + self::XLS_TYPE_MSODRAWINGGROUP => $xls->readMsoDrawingGroup(), + self::XLS_TYPE_SST => $xls->readSst(), + self::XLS_TYPE_EOF => $xls->readDefault(), + default => $xls->readDefault(), + }; + + if ($code === self::XLS_TYPE_EOF) { + break; + } + } + + // Resolve indexed colors for font, fill, and border colors + // Cannot be resolved already in XF record, because PALETTE record comes afterwards + if (!$xls->readDataOnly) { + foreach ($xls->objFonts as $objFont) { + if (isset($objFont->colorIndex)) { + $color = Color::map($objFont->colorIndex, $xls->palette, $xls->version); + $objFont->getColor()->setRGB($color['rgb']); + } + } + + foreach ($xls->spreadsheet->getCellXfCollection() as $objStyle) { + // fill start and end color + $fill = $objStyle->getFill(); + + if (isset($fill->startcolorIndex)) { + $startColor = Color::map($fill->startcolorIndex, $xls->palette, $xls->version); + $fill->getStartColor()->setRGB($startColor['rgb']); + } + if (isset($fill->endcolorIndex)) { + $endColor = Color::map($fill->endcolorIndex, $xls->palette, $xls->version); + $fill->getEndColor()->setRGB($endColor['rgb']); + } + + // border colors + $top = $objStyle->getBorders()->getTop(); + $right = $objStyle->getBorders()->getRight(); + $bottom = $objStyle->getBorders()->getBottom(); + $left = $objStyle->getBorders()->getLeft(); + $diagonal = $objStyle->getBorders()->getDiagonal(); + + if (isset($top->colorIndex)) { + $borderTopColor = Color::map($top->colorIndex, $xls->palette, $xls->version); + $top->getColor()->setRGB($borderTopColor['rgb']); + } + if (isset($right->colorIndex)) { + $borderRightColor = Color::map($right->colorIndex, $xls->palette, $xls->version); + $right->getColor()->setRGB($borderRightColor['rgb']); + } + if (isset($bottom->colorIndex)) { + $borderBottomColor = Color::map($bottom->colorIndex, $xls->palette, $xls->version); + $bottom->getColor()->setRGB($borderBottomColor['rgb']); + } + if (isset($left->colorIndex)) { + $borderLeftColor = Color::map($left->colorIndex, $xls->palette, $xls->version); + $left->getColor()->setRGB($borderLeftColor['rgb']); + } + if (isset($diagonal->colorIndex)) { + $borderDiagonalColor = Color::map($diagonal->colorIndex, $xls->palette, $xls->version); + $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']); + } + } + } + + // treat MSODRAWINGGROUP records, workbook-level Escher + $escherWorkbook = null; + if (!$xls->readDataOnly && $xls->drawingGroupData) { + $escher = new SharedEscher(); + $reader = new Escher($escher); + $escherWorkbook = $reader->load($xls->drawingGroupData); + } + + // Parse the individual sheets + $xls->activeSheetSet = false; + $sheetCreated = false; + foreach ($xls->sheets as $sheet) { + $selectedCells = ''; + if ($sheet['sheetType'] != 0x00) { + // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module + continue; + } + + // check if sheet should be skipped + if (isset($xls->loadSheetsOnly) && !in_array($sheet['name'], $xls->loadSheetsOnly)) { + continue; + } + + // add sheet to PhpSpreadsheet object + $xls->phpSheet = $xls->spreadsheet->createSheet(); + $sheetCreated = true; + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula + // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet + // name in line with the formula, not the reverse + $xls->phpSheet->setTitle($sheet['name'], false, false); + $xls->phpSheet->setSheetState($sheet['sheetState']); + + $xls->pos = $sheet['offset']; + + // Initialize isFitToPages. May change after reading SHEETPR record. + $xls->isFitToPages = false; + + // Initialize drawingData + $xls->drawingData = ''; + + // Initialize objs + $xls->objs = []; + + // Initialize shared formula parts + $xls->sharedFormulaParts = []; + + // Initialize shared formulas + $xls->sharedFormulas = []; + + // Initialize text objs + $xls->textObjects = []; + + // Initialize cell annotations + $xls->cellNotes = []; + $xls->textObjRef = -1; + + while ($xls->pos <= $xls->dataSize - 4) { + $code = self::getUInt2d($xls->data, $xls->pos); + + switch ($code) { + case self::XLS_TYPE_BOF: + $xls->readBof(); + + break; + case self::XLS_TYPE_PRINTGRIDLINES: + $xls->readPrintGridlines(); + + break; + case self::XLS_TYPE_DEFAULTROWHEIGHT: + $xls->readDefaultRowHeight(); + + break; + case self::XLS_TYPE_SHEETPR: + $xls->readSheetPr(); + + break; + case self::XLS_TYPE_HORIZONTALPAGEBREAKS: + $xls->readHorizontalPageBreaks(); + + break; + case self::XLS_TYPE_VERTICALPAGEBREAKS: + $xls->readVerticalPageBreaks(); + + break; + case self::XLS_TYPE_HEADER: + $xls->readHeader(); + + break; + case self::XLS_TYPE_FOOTER: + $xls->readFooter(); + + break; + case self::XLS_TYPE_HCENTER: + $xls->readHcenter(); + + break; + case self::XLS_TYPE_VCENTER: + $xls->readVcenter(); + + break; + case self::XLS_TYPE_LEFTMARGIN: + $xls->readLeftMargin(); + + break; + case self::XLS_TYPE_RIGHTMARGIN: + $xls->readRightMargin(); + + break; + case self::XLS_TYPE_TOPMARGIN: + $xls->readTopMargin(); + + break; + case self::XLS_TYPE_BOTTOMMARGIN: + $xls->readBottomMargin(); + + break; + case self::XLS_TYPE_PAGESETUP: + $xls->readPageSetup(); + + break; + case self::XLS_TYPE_PROTECT: + $xls->readProtect(); + + break; + case self::XLS_TYPE_SCENPROTECT: + $xls->readScenProtect(); + + break; + case self::XLS_TYPE_OBJECTPROTECT: + $xls->readObjectProtect(); + + break; + case self::XLS_TYPE_PASSWORD: + $xls->readPassword(); + + break; + case self::XLS_TYPE_DEFCOLWIDTH: + $xls->readDefColWidth(); + + break; + case self::XLS_TYPE_COLINFO: + $xls->readColInfo(); + + break; + case self::XLS_TYPE_DIMENSION: + $xls->readDefault(); + + break; + case self::XLS_TYPE_ROW: + $xls->readRow(); + + break; + case self::XLS_TYPE_DBCELL: + $xls->readDefault(); + + break; + case self::XLS_TYPE_RK: + $xls->readRk(); + + break; + case self::XLS_TYPE_LABELSST: + $xls->readLabelSst(); + + break; + case self::XLS_TYPE_MULRK: + $xls->readMulRk(); + + break; + case self::XLS_TYPE_NUMBER: + $xls->readNumber(); + + break; + case self::XLS_TYPE_FORMULA: + $xls->readFormula(); + + break; + case self::XLS_TYPE_SHAREDFMLA: + $xls->readSharedFmla(); + + break; + case self::XLS_TYPE_BOOLERR: + $xls->readBoolErr(); + + break; + case self::XLS_TYPE_MULBLANK: + $xls->readMulBlank(); + + break; + case self::XLS_TYPE_LABEL: + $xls->readLabel(); + + break; + case self::XLS_TYPE_BLANK: + $xls->readBlank(); + + break; + case self::XLS_TYPE_MSODRAWING: + $xls->readMsoDrawing(); + + break; + case self::XLS_TYPE_OBJ: + $xls->readObj(); + + break; + case self::XLS_TYPE_WINDOW2: + $xls->readWindow2(); + + break; + case self::XLS_TYPE_PAGELAYOUTVIEW: + $xls->readPageLayoutView(); + + break; + case self::XLS_TYPE_SCL: + $xls->readScl(); + + break; + case self::XLS_TYPE_PANE: + $xls->readPane(); + + break; + case self::XLS_TYPE_SELECTION: + $selectedCells = $xls->readSelection(); + + break; + case self::XLS_TYPE_MERGEDCELLS: + $xls->readMergedCells(); + + break; + case self::XLS_TYPE_HYPERLINK: + $xls->readHyperLink(); + + break; + case self::XLS_TYPE_DATAVALIDATIONS: + $xls->readDataValidations(); + + break; + case self::XLS_TYPE_DATAVALIDATION: + $xls->readDataValidation(); + + break; + case self::XLS_TYPE_CFHEADER: + /** @var string[] */ + $cellRangeAddresses = $xls->readCFHeader(); + + break; + case self::XLS_TYPE_CFRULE: + $xls->readCFRule($cellRangeAddresses ?? []); + + break; + case self::XLS_TYPE_SHEETLAYOUT: + $xls->readSheetLayout(); + + break; + case self::XLS_TYPE_SHEETPROTECTION: + $xls->readSheetProtection(); + + break; + case self::XLS_TYPE_RANGEPROTECTION: + $xls->readRangeProtection(); + + break; + case self::XLS_TYPE_NOTE: + $xls->readNote(); + + break; + case self::XLS_TYPE_TXO: + $xls->readTextObject(); + + break; + case self::XLS_TYPE_CONTINUE: + $xls->readContinue(); + + break; + case self::XLS_TYPE_EOF: + $xls->readDefault(); + + break 2; + default: + $xls->readDefault(); + + break; + } + } + + // treat MSODRAWING records, sheet-level Escher + if (!$xls->readDataOnly && $xls->drawingData) { + $escherWorksheet = new SharedEscher(); + $reader = new Escher($escherWorksheet); + $escherWorksheet = $reader->load($xls->drawingData); + + // get all spContainers in one long array, so they can be mapped to OBJ records + /** @var SpContainer[] $allSpContainers */ + $allSpContainers = $escherWorksheet->getDgContainerOrThrow()->getSpgrContainerOrThrow()->getAllSpContainers(); + } + + // treat OBJ records + foreach ($xls->objs as $n => $obj) { + // the first shape container never has a corresponding OBJ record, hence $n + 1 + if (isset($allSpContainers[$n + 1])) { + $spContainer = $allSpContainers[$n + 1]; + + // we skip all spContainers that are a part of a group shape since we cannot yet handle those + if ($spContainer->getNestingLevel() > 1) { + continue; + } + + // calculate the width and height of the shape + /** @var int $startRow */ + [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates()); + /** @var int $endRow */ + [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates()); + + $startOffsetX = $spContainer->getStartOffsetX(); + $startOffsetY = $spContainer->getStartOffsetY(); + $endOffsetX = $spContainer->getEndOffsetX(); + $endOffsetY = $spContainer->getEndOffsetY(); + + $width = SharedXls::getDistanceX($xls->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX); + $height = SharedXls::getDistanceY($xls->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY); + + // calculate offsetX and offsetY of the shape + $offsetX = (int) ($startOffsetX * SharedXls::sizeCol($xls->phpSheet, $startColumn) / 1024); + $offsetY = (int) ($startOffsetY * SharedXls::sizeRow($xls->phpSheet, $startRow) / 256); + + /** @var int[] $obj */ + switch ($obj['otObjType']) { + case 0x19: + // Note + if (isset($xls->cellNotes[$obj['idObjID']])) { + //$cellNote = $xls->cellNotes[$obj['idObjID']]; + + if (isset($xls->textObjects[$obj['idObjID']])) { + $textObject = $xls->textObjects[$obj['idObjID']]; + $xls->cellNotes[$obj['idObjID']]['objTextData'] = $textObject; //* @phpstan-ignore-line + } + } + + break; + case 0x08: + // picture + // get index to BSE entry (1-based) + /** @var int */ + $BSEindex = $spContainer->getOPT(0x0104); + + // If there is no BSE Index, we will fail here and other fields are not read. + // Fix by checking here. + // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field? + // More likely: an incompatible picture + if (!$BSEindex) { + continue 2; + } + + if ($escherWorkbook) { + /** @var BSE[] */ + $BSECollection = $escherWorkbook->getDggContainerOrThrow()->getBstoreContainerOrThrow()->getBSECollection(); + $BSE = $BSECollection[$BSEindex - 1]; + $blipType = $BSE->getBlipType(); + + // need check because some blip types are not supported by Escher reader such as EMF + if ($blip = $BSE->getBlip()) { + $ih = imagecreatefromstring($blip->getData()); + if ($ih !== false) { + $drawing = new MemoryDrawing(); + $drawing->setImageResource($ih); + + // width, height, offsetX, offsetY + $drawing->setResizeProportional(false); + $drawing->setWidth($width); + $drawing->setHeight($height); + $drawing->setOffsetX($offsetX); + $drawing->setOffsetY($offsetY); + + switch ($blipType) { + case BSE::BLIPTYPE_JPEG: + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); + + break; + case BSE::BLIPTYPE_PNG: + imagealphablending($ih, false); + imagesavealpha($ih, true); + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); + + break; + } + + $drawing->setWorksheet($xls->phpSheet); + $drawing->setCoordinates($spContainer->getStartCoordinates()); + } + } + } + + break; + default: + // other object type + break; + } + } + } + + // treat SHAREDFMLA records + if ($xls->version == self::XLS_BIFF8) { + foreach ($xls->sharedFormulaParts as $cell => $baseCell) { + /** @var int $row */ + [$column, $row] = Coordinate::coordinateFromString($cell); + /** @var string $baseCell */ + if ($xls->getReadFilter()->readCell($column, $row, $xls->phpSheet->getTitle())) { + /** @var string */ + $temp = $xls->sharedFormulas[$baseCell]; + $formula = $xls->getFormulaFromStructure($temp, $cell); + $xls->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA); + } + } + } + + if (!empty($xls->cellNotes)) { + foreach ($xls->cellNotes as $note => $noteDetails) { + /** @var array{author: string, cellRef: string, objTextData?: mixed[]} $noteDetails */ + if (!isset($noteDetails['objTextData'])) { + if (isset($xls->textObjects[$note])) { + $textObject = $xls->textObjects[$note]; + $noteDetails['objTextData'] = $textObject; + } else { + $noteDetails['objTextData']['text'] = ''; + } + } + $cellAddress = str_replace('$', '', $noteDetails['cellRef']); + /** @var string */ + $tempDetails = $noteDetails['objTextData']['text']; + $xls->phpSheet + ->getComment($cellAddress) + ->setAuthor($noteDetails['author']) + ->setText( + $xls->parseRichText($tempDetails) + ); + } + } + if ($selectedCells !== '') { + $xls->phpSheet->setSelectedCells($selectedCells); + } + } + if ($xls->createBlankSheetIfNoneRead && !$sheetCreated) { + $xls->spreadsheet->createSheet(); + } + if ($xls->activeSheetSet === false) { + $xls->spreadsheet->setActiveSheetIndex(0); + } + + // add the named ranges (defined names) + foreach ($xls->definedname as $definedName) { + /** @var array{isBuiltInName: int, name: string, formula: string, scope: int} $definedName */ + if ($definedName['isBuiltInName']) { + switch ($definedName['name']) { + case pack('C', 0x06): + // print area + // in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2 + $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma? + + $extractedRanges = []; + $sheetName = ''; + /** @var non-empty-string $range */ + foreach ($ranges as $range) { + // $range should look like one of these + // Foo!$C$7:$J$66 + // Bar!$A$1:$IV$2 + $explodes = Worksheet::extractSheetTitle($range, true, true); + $sheetName = (string) $explodes[0]; + if (!str_contains($explodes[1], ':')) { + $explodes[1] = $explodes[1] . ':' . $explodes[1]; + } + $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66 + } + if ($docSheet = $xls->spreadsheet->getSheetByName($sheetName)) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2 + } + + break; + case pack('C', 0x07): + // print titles (repeating rows) + // Assuming BIFF8, there are 3 cases + // 1. repeating rows + // formula looks like this: Sheet!$A$1:$IV$2 + // rows 1-2 repeat + // 2. repeating columns + // formula looks like this: Sheet!$A$1:$B$65536 + // columns A-B repeat + // 3. both repeating rows and repeating columns + // formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2 + $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma? + foreach ($ranges as $range) { + // $range should look like this one of these + // Sheet!$A$1:$B$65536 + // Sheet!$A$1:$IV$2 + if (str_contains($range, '!')) { + $explodes = Worksheet::extractSheetTitle($range, true, true); + $docSheet = $xls->spreadsheet->getSheetByName($explodes[0]); + if ($docSheet) { + $extractedRange = $explodes[1]; + $extractedRange = str_replace('$', '', $extractedRange); + + $coordinateStrings = explode(':', $extractedRange); + if (count($coordinateStrings) == 2) { + [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]); + [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]); + $firstRow = (int) $firstRow; + $lastRow = (int) $lastRow; + + if ($firstColumn == 'A' && $lastColumn == 'IV') { + // then we have repeating rows + $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]); + } elseif ($firstRow == 1 && $lastRow == 65536) { + // then we have repeating columns + $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]); + } + } + } + } + } + + break; + } + } else { + // Extract range + $formula = $definedName['formula']; + if (str_contains($formula, '!')) { + $explodes = Worksheet::extractSheetTitle($formula, true, true); + $docSheet = $xls->spreadsheet->getSheetByName($explodes[0]); + if ($docSheet) { + $extractedRange = $explodes[1]; + + $localOnly = ($definedName['scope'] === 0) ? false : true; + + $scope = ($definedName['scope'] === 0) ? null : $xls->spreadsheet->getSheetByName($xls->sheets[$definedName['scope'] - 1]['name']); + + $xls->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope)); + } + } + // Named Value + // TODO Provide support for named values + } + } + $xls->data = ''; + + return $xls->spreadsheet; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/MD5.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/MD5.php new file mode 100644 index 0000000..56dd4a3 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/MD5.php @@ -0,0 +1,198 @@ +reset(); + } + + /** + * Reset the MD5 stream context. + */ + public function reset(): void + { + $this->a = 0x67452301; + $this->b = self::signedInt(0xEFCDAB89); + $this->c = self::signedInt(0x98BADCFE); + $this->d = 0x10325476; + } + + /** + * Get MD5 stream context. + */ + public function getContext(): string + { + $s = ''; + foreach (['a', 'b', 'c', 'd'] as $i) { + $v = $this->{$i}; + $s .= chr($v & 0xFF); + $s .= chr(($v >> 8) & 0xFF); + $s .= chr(($v >> 16) & 0xFF); + $s .= chr(($v >> 24) & 0xFF); + } + + return $s; + } + + /** + * Add data to context. + * + * @param string $data Data to add + */ + public function add(string $data): void + { + $unpacked = unpack('V16', $data) ?: throw new ReaderException('unable to unpack data'); + /** @var int[] */ + $words = array_values($unpacked); + + $A = $this->a; + $B = $this->b; + $C = $this->c; + $D = $this->d; + + $F = [self::class, 'f']; + $G = [self::class, 'g']; + $H = [self::class, 'h']; + $I = [self::class, 'i']; + + // ROUND 1 + self::step($F, $A, $B, $C, $D, $words[0], 7, 0xD76AA478); + self::step($F, $D, $A, $B, $C, $words[1], 12, 0xE8C7B756); + self::step($F, $C, $D, $A, $B, $words[2], 17, 0x242070DB); + self::step($F, $B, $C, $D, $A, $words[3], 22, 0xC1BDCEEE); + self::step($F, $A, $B, $C, $D, $words[4], 7, 0xF57C0FAF); + self::step($F, $D, $A, $B, $C, $words[5], 12, 0x4787C62A); + self::step($F, $C, $D, $A, $B, $words[6], 17, 0xA8304613); + self::step($F, $B, $C, $D, $A, $words[7], 22, 0xFD469501); + self::step($F, $A, $B, $C, $D, $words[8], 7, 0x698098D8); + self::step($F, $D, $A, $B, $C, $words[9], 12, 0x8B44F7AF); + self::step($F, $C, $D, $A, $B, $words[10], 17, 0xFFFF5BB1); + self::step($F, $B, $C, $D, $A, $words[11], 22, 0x895CD7BE); + self::step($F, $A, $B, $C, $D, $words[12], 7, 0x6B901122); + self::step($F, $D, $A, $B, $C, $words[13], 12, 0xFD987193); + self::step($F, $C, $D, $A, $B, $words[14], 17, 0xA679438E); + self::step($F, $B, $C, $D, $A, $words[15], 22, 0x49B40821); + + // ROUND 2 + self::step($G, $A, $B, $C, $D, $words[1], 5, 0xF61E2562); + self::step($G, $D, $A, $B, $C, $words[6], 9, 0xC040B340); + self::step($G, $C, $D, $A, $B, $words[11], 14, 0x265E5A51); + self::step($G, $B, $C, $D, $A, $words[0], 20, 0xE9B6C7AA); + self::step($G, $A, $B, $C, $D, $words[5], 5, 0xD62F105D); + self::step($G, $D, $A, $B, $C, $words[10], 9, 0x02441453); + self::step($G, $C, $D, $A, $B, $words[15], 14, 0xD8A1E681); + self::step($G, $B, $C, $D, $A, $words[4], 20, 0xE7D3FBC8); + self::step($G, $A, $B, $C, $D, $words[9], 5, 0x21E1CDE6); + self::step($G, $D, $A, $B, $C, $words[14], 9, 0xC33707D6); + self::step($G, $C, $D, $A, $B, $words[3], 14, 0xF4D50D87); + self::step($G, $B, $C, $D, $A, $words[8], 20, 0x455A14ED); + self::step($G, $A, $B, $C, $D, $words[13], 5, 0xA9E3E905); + self::step($G, $D, $A, $B, $C, $words[2], 9, 0xFCEFA3F8); + self::step($G, $C, $D, $A, $B, $words[7], 14, 0x676F02D9); + self::step($G, $B, $C, $D, $A, $words[12], 20, 0x8D2A4C8A); + + // ROUND 3 + self::step($H, $A, $B, $C, $D, $words[5], 4, 0xFFFA3942); + self::step($H, $D, $A, $B, $C, $words[8], 11, 0x8771F681); + self::step($H, $C, $D, $A, $B, $words[11], 16, 0x6D9D6122); + self::step($H, $B, $C, $D, $A, $words[14], 23, 0xFDE5380C); + self::step($H, $A, $B, $C, $D, $words[1], 4, 0xA4BEEA44); + self::step($H, $D, $A, $B, $C, $words[4], 11, 0x4BDECFA9); + self::step($H, $C, $D, $A, $B, $words[7], 16, 0xF6BB4B60); + self::step($H, $B, $C, $D, $A, $words[10], 23, 0xBEBFBC70); + self::step($H, $A, $B, $C, $D, $words[13], 4, 0x289B7EC6); + self::step($H, $D, $A, $B, $C, $words[0], 11, 0xEAA127FA); + self::step($H, $C, $D, $A, $B, $words[3], 16, 0xD4EF3085); + self::step($H, $B, $C, $D, $A, $words[6], 23, 0x04881D05); + self::step($H, $A, $B, $C, $D, $words[9], 4, 0xD9D4D039); + self::step($H, $D, $A, $B, $C, $words[12], 11, 0xE6DB99E5); + self::step($H, $C, $D, $A, $B, $words[15], 16, 0x1FA27CF8); + self::step($H, $B, $C, $D, $A, $words[2], 23, 0xC4AC5665); + + // ROUND 4 + self::step($I, $A, $B, $C, $D, $words[0], 6, 0xF4292244); + self::step($I, $D, $A, $B, $C, $words[7], 10, 0x432AFF97); + self::step($I, $C, $D, $A, $B, $words[14], 15, 0xAB9423A7); + self::step($I, $B, $C, $D, $A, $words[5], 21, 0xFC93A039); + self::step($I, $A, $B, $C, $D, $words[12], 6, 0x655B59C3); + self::step($I, $D, $A, $B, $C, $words[3], 10, 0x8F0CCC92); + self::step($I, $C, $D, $A, $B, $words[10], 15, 0xFFEFF47D); + self::step($I, $B, $C, $D, $A, $words[1], 21, 0x85845DD1); + self::step($I, $A, $B, $C, $D, $words[8], 6, 0x6FA87E4F); + self::step($I, $D, $A, $B, $C, $words[15], 10, 0xFE2CE6E0); + self::step($I, $C, $D, $A, $B, $words[6], 15, 0xA3014314); + self::step($I, $B, $C, $D, $A, $words[13], 21, 0x4E0811A1); + self::step($I, $A, $B, $C, $D, $words[4], 6, 0xF7537E82); + self::step($I, $D, $A, $B, $C, $words[11], 10, 0xBD3AF235); + self::step($I, $C, $D, $A, $B, $words[2], 15, 0x2AD7D2BB); + self::step($I, $B, $C, $D, $A, $words[9], 21, 0xEB86D391); + + $this->a = ($this->a + $A) & self::$allOneBits; + $this->b = ($this->b + $B) & self::$allOneBits; + $this->c = ($this->c + $C) & self::$allOneBits; + $this->d = ($this->d + $D) & self::$allOneBits; + } + + private static function f(int $X, int $Y, int $Z): int + { + return ($X & $Y) | ((~$X) & $Z); // X AND Y OR NOT X AND Z + } + + private static function g(int $X, int $Y, int $Z): int + { + return ($X & $Z) | ($Y & (~$Z)); // X AND Z OR Y AND NOT Z + } + + private static function h(int $X, int $Y, int $Z): int + { + return $X ^ $Y ^ $Z; // X XOR Y XOR Z + } + + private static function i(int $X, int $Y, int $Z): int + { + return $Y ^ ($X | (~$Z)); // Y XOR (X OR NOT Z) + } + + /** @param float|int $t may be float on 32-bit system */ + private static function step(callable $func, int &$A, int $B, int $C, int $D, int $M, int $s, $t): void + { + $t = self::signedInt($t); + /** @var int */ + $temp = call_user_func($func, $B, $C, $D); + $A = (int) ($A + $temp + $M + $t) & self::$allOneBits; + $A = self::rotate($A, $s); + $A = (int) ($B + $A) & self::$allOneBits; + } + + /** @param float|int $result may be float on 32-bit system */ + private static function signedInt($result): int + { + return is_int($result) ? $result : (int) (PHP_INT_MIN + $result - 1 - PHP_INT_MAX); + } + + private static function rotate(int $decimal, int $bits): int + { + $binary = str_pad(decbin($decimal), 32, '0', STR_PAD_LEFT); + + return self::signedInt(bindec(substr($binary, $bits) . substr($binary, 0, $bits))); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Mappings.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Mappings.php new file mode 100644 index 0000000..7a6be8e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Mappings.php @@ -0,0 +1,271 @@ + ['ISNA', 1], + 3 => ['ISERROR', 1], + 10 => ['NA', 0], + 15 => ['SIN', 1], + 16 => ['COS', 1], + 17 => ['TAN', 1], + 18 => ['ATAN', 1], + 19 => ['PI', 0], + 20 => ['SQRT', 1], + 21 => ['EXP', 1], + 22 => ['LN', 1], + 23 => ['LOG10', 1], + 24 => ['ABS', 1], + 25 => ['INT', 1], + 26 => ['SIGN', 1], + 27 => ['ROUND', 2], + 30 => ['REPT', 2], + 31 => ['MID', 3], + 32 => ['LEN', 1], + 33 => ['VALUE', 1], + 34 => ['TRUE', 0], + 35 => ['FALSE', 0], + 38 => ['NOT', 1], + 39 => ['MOD', 2], + 40 => ['DCOUNT', 3], + 41 => ['DSUM', 3], + 42 => ['DAVERAGE', 3], + 43 => ['DMIN', 3], + 44 => ['DMAX', 3], + 45 => ['DSTDEV', 3], + 48 => ['TEXT', 2], + 61 => ['MIRR', 3], + 63 => ['RAND', 0], + 65 => ['DATE', 3], + 66 => ['TIME', 3], + 67 => ['DAY', 1], + 68 => ['MONTH', 1], + 69 => ['YEAR', 1], + 71 => ['HOUR', 1], + 72 => ['MINUTE', 1], + 73 => ['SECOND', 1], + 74 => ['NOW', 0], + 75 => ['AREAS', 1], + 76 => ['ROWS', 1], + 77 => ['COLUMNS', 1], + 83 => ['TRANSPOSE', 1], + 86 => ['TYPE', 1], + 97 => ['ATAN2', 2], + 98 => ['ASIN', 1], + 99 => ['ACOS', 1], + 105 => ['ISREF', 1], + 111 => ['CHAR', 1], + 112 => ['LOWER', 1], + 113 => ['UPPER', 1], + 114 => ['PROPER', 1], + 117 => ['EXACT', 2], + 118 => ['TRIM', 1], + 119 => ['REPLACE', 4], + 121 => ['CODE', 1], + 126 => ['ISERR', 1], + 127 => ['ISTEXT', 1], + 128 => ['ISNUMBER', 1], + 129 => ['ISBLANK', 1], + 130 => ['T', 1], + 131 => ['N', 1], + 140 => ['DATEVALUE', 1], + 141 => ['TIMEVALUE', 1], + 142 => ['SLN', 3], + 143 => ['SYD', 4], + 162 => ['CLEAN', 1], + 163 => ['MDETERM', 1], + 164 => ['MINVERSE', 1], + 165 => ['MMULT', 2], + 184 => ['FACT', 1], + 189 => ['DPRODUCT', 3], + 190 => ['ISNONTEXT', 1], + 195 => ['DSTDEVP', 3], + 196 => ['DVARP', 3], + 198 => ['ISLOGICAL', 1], + 199 => ['DCOUNTA', 3], + 207 => ['REPLACEB', 4], + 210 => ['MIDB', 3], + 211 => ['LENB', 1], + 212 => ['ROUNDUP', 2], + 213 => ['ROUNDDOWN', 2], + 214 => ['ASC', 1], + 215 => ['DBCS', 1], + 221 => ['TODAY', 0], + 229 => ['SINH', 1], + 230 => ['COSH', 1], + 231 => ['TANH', 1], + 232 => ['ASINH', 1], + 233 => ['ACOSH', 1], + 234 => ['ATANH', 1], + 235 => ['DGET', 3], + 244 => ['INFO', 1], + 252 => ['FREQUENCY', 2], + 261 => ['ERROR.TYPE', 1], + 271 => ['GAMMALN', 1], + 273 => ['BINOMDIST', 4], + 274 => ['CHIDIST', 2], + 275 => ['CHIINV', 2], + 276 => ['COMBIN', 2], + 277 => ['CONFIDENCE', 3], + 278 => ['CRITBINOM', 3], + 279 => ['EVEN', 1], + 280 => ['EXPONDIST', 3], + 281 => ['FDIST', 3], + 282 => ['FINV', 3], + 283 => ['FISHER', 1], + 284 => ['FISHERINV', 1], + 285 => ['FLOOR', 2], + 286 => ['GAMMADIST', 4], + 287 => ['GAMMAINV', 3], + 288 => ['CEILING', 2], + 289 => ['HYPGEOMDIST', 4], + 290 => ['LOGNORMDIST', 3], + 291 => ['LOGINV', 3], + 292 => ['NEGBINOMDIST', 3], + 293 => ['NORMDIST', 4], + 294 => ['NORMSDIST', 1], + 295 => ['NORMINV', 3], + 296 => ['NORMSINV', 1], + 297 => ['STANDARDIZE', 3], + 298 => ['ODD', 1], + 299 => ['PERMUT', 2], + 300 => ['POISSON', 3], + 301 => ['TDIST', 3], + 302 => ['WEIBULL', 4], + 303 => ['SUMXMY2', 2], + 304 => ['SUMX2MY2', 2], + 305 => ['SUMX2PY2', 2], + 306 => ['CHITEST', 2], + 307 => ['CORREL', 2], + 308 => ['COVAR', 2], + 309 => ['FORECAST', 3], + 310 => ['FTEST', 2], + 311 => ['INTERCEPT', 2], + 312 => ['PEARSON', 2], + 313 => ['RSQ', 2], + 314 => ['STEYX', 2], + 315 => ['SLOPE', 2], + 316 => ['TTEST', 4], + 325 => ['LARGE', 2], + 326 => ['SMALL', 2], + 327 => ['QUARTILE', 2], + 328 => ['PERCENTILE', 2], + 331 => ['TRIMMEAN', 2], + 332 => ['TINV', 2], + 337 => ['POWER', 2], + 342 => ['RADIANS', 1], + 343 => ['DEGREES', 1], + 346 => ['COUNTIF', 2], + 347 => ['COUNTBLANK', 1], + 350 => ['ISPMT', 4], + 351 => ['DATEDIF', 3], + 352 => ['DATESTRING', 1], + 353 => ['NUMBERSTRING', 2], + 360 => ['PHONETIC', 1], + 368 => ['BAHTTEXT', 1], + ]; + + /** + * Map tFuncV values (functions with variable number of arguments). + * Key is tFuncV value. + * Value is Excel function name. + */ + const TFUNCV_MAPPINGS = [ + 0 => 'COUNT', + 1 => 'IF', + 4 => 'SUM', + 5 => 'AVERAGE', + 6 => 'MIN', + 7 => 'MAX', + 8 => 'ROW', + 9 => 'COLUMN', + 11 => 'NPV', + 12 => 'STDEV', + 13 => 'DOLLAR', + 14 => 'FIXED', + 28 => 'LOOKUP', + 29 => 'INDEX', + 36 => 'AND', + 37 => 'OR', + 46 => 'VAR', + 49 => 'LINEST', + 50 => 'TREND', + 51 => 'LOGEST', + 52 => 'GROWTH', + 56 => 'PV', + 57 => 'FV', + 58 => 'NPER', + 59 => 'PMT', + 60 => 'RATE', + 62 => 'IRR', + 64 => 'MATCH', + 70 => 'WEEKDAY', + 78 => 'OFFSET', + 82 => 'SEARCH', + 100 => 'CHOOSE', + 101 => 'HLOOKUP', + 102 => 'VLOOKUP', + 109 => 'LOG', + 115 => 'LEFT', + 116 => 'RIGHT', + 120 => 'SUBSTITUTE', + 124 => 'FIND', + 125 => 'CELL', + 144 => 'DDB', + 148 => 'INDIRECT', + 167 => 'IPMT', + 168 => 'PPMT', + 169 => 'COUNTA', + 183 => 'PRODUCT', + 193 => 'STDEVP', + 194 => 'VARP', + 197 => 'TRUNC', + 204 => 'USDOLLAR', + 205 => 'FINDB', + 206 => 'SEARCHB', + 208 => 'LEFTB', + 209 => 'RIGHTB', + 216 => 'RANK', + 219 => 'ADDRESS', + 220 => 'DAYS360', + 222 => 'VDB', + 227 => 'MEDIAN', + 228 => 'SUMPRODUCT', + 247 => 'DB', + 255 => '', + 269 => 'AVEDEV', + 270 => 'BETADIST', + 272 => 'BETAINV', + 317 => 'PROB', + 318 => 'DEVSQ', + 319 => 'GEOMEAN', + 320 => 'HARMEAN', + 321 => 'SUMSQ', + 322 => 'KURT', + 323 => 'SKEW', + 324 => 'ZTEST', + 329 => 'PERCENTRANK', + 330 => 'MODE', + 336 => 'CONCATENATE', + 344 => 'SUBTOTAL', + 345 => 'SUMIF', + 354 => 'ROMAN', + 358 => 'GETPIVOTDATA', + 359 => 'HYPERLINK', + 361 => 'AVERAGEA', + 362 => 'MAXA', + 363 => 'MINA', + 364 => 'STDEVPA', + 365 => 'VARPA', + 366 => 'STDEVA', + 367 => 'VARA', + ]; +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/RC4.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/RC4.php new file mode 100644 index 0000000..a880eee --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/RC4.php @@ -0,0 +1,59 @@ +i = 0; $this->i < 256; ++$this->i) { + $this->s[$this->i] = $this->i; + } + + $this->j = 0; + for ($this->i = 0; $this->i < 256; ++$this->i) { + $this->j = ($this->j + $this->s[$this->i] + ord($key[$this->i % $len])) % 256; + $t = $this->s[$this->i]; + $this->s[$this->i] = $this->s[$this->j]; + $this->s[$this->j] = $t; + } + $this->i = $this->j = 0; + } + + /** + * Symmetric decryption/encryption function. + * + * @param string $data Data to encrypt/decrypt + */ + public function RC4(string $data): string + { + $len = strlen($data); + for ($c = 0; $c < $len; ++$c) { + $this->i = ($this->i + 1) % 256; + $this->j = ($this->j + $this->s[$this->i]) % 256; + $t = $this->s[$this->i]; + $this->s[$this->i] = $this->s[$this->j]; + $this->s[$this->j] = $t; + + $t = ($this->s[$this->i] + $this->s[$this->j]) % 256; + + $data[$c] = chr(ord($data[$c]) ^ $this->s[$t]); + } + + return $data; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/Border.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/Border.php new file mode 100644 index 0000000..96275bf --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/Border.php @@ -0,0 +1,33 @@ + + */ + protected static array $borderStyleMap = [ + 0x00 => StyleBorder::BORDER_NONE, + 0x01 => StyleBorder::BORDER_THIN, + 0x02 => StyleBorder::BORDER_MEDIUM, + 0x03 => StyleBorder::BORDER_DASHED, + 0x04 => StyleBorder::BORDER_DOTTED, + 0x05 => StyleBorder::BORDER_THICK, + 0x06 => StyleBorder::BORDER_DOUBLE, + 0x07 => StyleBorder::BORDER_HAIR, + 0x08 => StyleBorder::BORDER_MEDIUMDASHED, + 0x09 => StyleBorder::BORDER_DASHDOT, + 0x0A => StyleBorder::BORDER_MEDIUMDASHDOT, + 0x0B => StyleBorder::BORDER_DASHDOTDOT, + 0x0C => StyleBorder::BORDER_MEDIUMDASHDOTDOT, + 0x0D => StyleBorder::BORDER_SLANTDASHDOT, + ]; + + public static function lookup(int $index): string + { + return self::$borderStyleMap[$index] ?? StyleBorder::BORDER_NONE; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/CellAlignment.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/CellAlignment.php new file mode 100644 index 0000000..6b89d27 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/CellAlignment.php @@ -0,0 +1,50 @@ + + */ + protected static array $horizontalAlignmentMap = [ + 0 => Alignment::HORIZONTAL_GENERAL, + 1 => Alignment::HORIZONTAL_LEFT, + 2 => Alignment::HORIZONTAL_CENTER, + 3 => Alignment::HORIZONTAL_RIGHT, + 4 => Alignment::HORIZONTAL_FILL, + 5 => Alignment::HORIZONTAL_JUSTIFY, + 6 => Alignment::HORIZONTAL_CENTER_CONTINUOUS, + ]; + + /** + * @var array + */ + protected static array $verticalAlignmentMap = [ + 0 => Alignment::VERTICAL_TOP, + 1 => Alignment::VERTICAL_CENTER, + 2 => Alignment::VERTICAL_BOTTOM, + 3 => Alignment::VERTICAL_JUSTIFY, + ]; + + public static function horizontal(Alignment $alignment, int $horizontal): void + { + if (array_key_exists($horizontal, self::$horizontalAlignmentMap)) { + $alignment->setHorizontal(self::$horizontalAlignmentMap[$horizontal]); + } + } + + public static function vertical(Alignment $alignment, int $vertical): void + { + if (array_key_exists($vertical, self::$verticalAlignmentMap)) { + $alignment->setVertical(self::$verticalAlignmentMap[$vertical]); + } + } + + public static function wrap(Alignment $alignment, int $wrap): void + { + $alignment->setWrapText((bool) $wrap); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/CellFont.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/CellFont.php new file mode 100644 index 0000000..2c6d2f7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/CellFont.php @@ -0,0 +1,39 @@ +setSuperscript(true); + + break; + case 0x0002: + $font->setSubscript(true); + + break; + } + } + + /** + * @var array + */ + protected static array $underlineMap = [ + 0x01 => Font::UNDERLINE_SINGLE, + 0x02 => Font::UNDERLINE_DOUBLE, + 0x21 => Font::UNDERLINE_SINGLEACCOUNTING, + 0x22 => Font::UNDERLINE_DOUBLEACCOUNTING, + ]; + + public static function underline(Font $font, int $underline): void + { + if (array_key_exists($underline, self::$underlineMap)) { + $font->setUnderline(self::$underlineMap[$underline]); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php new file mode 100644 index 0000000..93e3f44 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php @@ -0,0 +1,42 @@ + + */ + protected static array $fillPatternMap = [ + 0x00 => Fill::FILL_NONE, + 0x01 => Fill::FILL_SOLID, + 0x02 => Fill::FILL_PATTERN_MEDIUMGRAY, + 0x03 => Fill::FILL_PATTERN_DARKGRAY, + 0x04 => Fill::FILL_PATTERN_LIGHTGRAY, + 0x05 => Fill::FILL_PATTERN_DARKHORIZONTAL, + 0x06 => Fill::FILL_PATTERN_DARKVERTICAL, + 0x07 => Fill::FILL_PATTERN_DARKDOWN, + 0x08 => Fill::FILL_PATTERN_DARKUP, + 0x09 => Fill::FILL_PATTERN_DARKGRID, + 0x0A => Fill::FILL_PATTERN_DARKTRELLIS, + 0x0B => Fill::FILL_PATTERN_LIGHTHORIZONTAL, + 0x0C => Fill::FILL_PATTERN_LIGHTVERTICAL, + 0x0D => Fill::FILL_PATTERN_LIGHTDOWN, + 0x0E => Fill::FILL_PATTERN_LIGHTUP, + 0x0F => Fill::FILL_PATTERN_LIGHTGRID, + 0x10 => Fill::FILL_PATTERN_LIGHTTRELLIS, + 0x11 => Fill::FILL_PATTERN_GRAY125, + 0x12 => Fill::FILL_PATTERN_GRAY0625, + ]; + + /** + * Get fill pattern from index + * OpenOffice documentation: 2.5.12. + */ + public static function lookup(int $index): string + { + return self::$fillPatternMap[$index] ?? Fill::FILL_NONE; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/XlsBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/XlsBase.php new file mode 100644 index 0000000..eedd185 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/XlsBase.php @@ -0,0 +1,418 @@ + 0x00, + Border::BORDER_THIN, // => 0x01, + Border::BORDER_MEDIUM, // => 0x02, + Border::BORDER_DASHED, // => 0x03, + Border::BORDER_DOTTED, // => 0x04, + Border::BORDER_THICK, // => 0x05, + Border::BORDER_DOUBLE, // => 0x06, + Border::BORDER_HAIR, // => 0x07, + Border::BORDER_MEDIUMDASHED, // => 0x08, + Border::BORDER_DASHDOT, // => 0x09, + Border::BORDER_MEDIUMDASHDOT, // => 0x0A, + Border::BORDER_DASHDOTDOT, // => 0x0B, + Border::BORDER_MEDIUMDASHDOTDOT, // => 0x0C, + Border::BORDER_SLANTDASHDOT, // => 0x0D, + Border::BORDER_OMIT, // => 0x0E, + Border::BORDER_OMIT, // => 0x0F, + ]; + + /** + * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95) + * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'. + */ + protected string $codepage = ''; + + public function setCodepage(string $codepage): void + { + if (CodePage::validate($codepage) === false) { + throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage); + } + + $this->codepage = $codepage; + } + + public function getCodepage(): string + { + return $this->codepage; + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + if (File::testFileNoThrow($filename) === false) { + return false; + } + + try { + // Use ParseXL for the hard work. + $ole = new OLERead(); + + // get excel data + $ole->read($filename); + if ($ole->wrkbook === null) { + throw new Exception('The filename ' . $filename . ' is not recognised as a Spreadsheet file'); + } + + return true; + } catch (PhpSpreadsheetException) { + return false; + } + } + + /** + * Extract RGB color + * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4. + * + * @param string $rgb Encoded RGB value (4 bytes) + * + * @return array{rgb: string} + */ + protected static function readRGB(string $rgb): array + { + // offset: 0; size 1; Red component + $r = ord($rgb[0]); + + // offset: 1; size: 1; Green component + $g = ord($rgb[1]); + + // offset: 2; size: 1; Blue component + $b = ord($rgb[2]); + + // HEX notation, e.g. 'FF00FC' + $rgb = sprintf('%02X%02X%02X', $r, $g, $b); + + return ['rgb' => $rgb]; + } + + /** + * Extracts an Excel Unicode short string (8-bit string length) + * OpenOffice documentation: 2.5.3 + * function will automatically find out where the Unicode string ends. + * + * @return array{value: string, size: int} + */ + protected static function readUnicodeStringShort(string $subData): array + { + // offset: 0: size: 1; length of the string (character count) + $characterCount = ord($subData[0]); + + $string = self::readUnicodeString(substr($subData, 1), $characterCount); + + // add 1 for the string length + ++$string['size']; + + return $string; + } + + /** + * Extracts an Excel Unicode long string (16-bit string length) + * OpenOffice documentation: 2.5.3 + * this function is under construction, needs to support rich text, and Asian phonetic settings. + * + * @return array{value: string, size: int} + */ + protected static function readUnicodeStringLong(string $subData): array + { + // offset: 0: size: 2; length of the string (character count) + $characterCount = self::getUInt2d($subData, 0); + + $string = self::readUnicodeString(substr($subData, 2), $characterCount); + + // add 2 for the string length + $string['size'] += 2; + + return $string; + } + + /** + * Read Unicode string with no string length field, but with known character count + * this function is under construction, needs to support rich text, and Asian phonetic settings + * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3. + * + * @return array{value: string, size: int} + */ + protected static function readUnicodeString(string $subData, int $characterCount): array + { + // offset: 0: size: 1; option flags + // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit) + $isCompressed = !((0x01 & ord($subData[0])) >> 0); + + // bit: 2; mask: 0x04; Asian phonetic settings + //$hasAsian = (0x04) & ord($subData[0]) >> 2; + + // bit: 3; mask: 0x08; Rich-Text settings + //$hasRichText = (0x08) & ord($subData[0]) >> 3; + + // offset: 1: size: var; character array + // this offset assumes richtext and Asian phonetic settings are off which is generally wrong + // needs to be fixed + $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed); + + return [ + 'value' => $value, + 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags + ]; + } + + /** + * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas. + * Example: hello"world --> "hello""world". + * + * @param string $value UTF-8 encoded string + */ + protected static function UTF8toExcelDoubleQuoted(string $value): string + { + return '"' . str_replace('"', '""', $value) . '"'; + } + + /** + * Reads first 8 bytes of a string and return IEEE 754 float. + * + * @param string $data Binary string that is at least 8 bytes long + */ + protected static function extractNumber(string $data): int|float + { + $rknumhigh = self::getInt4d($data, 4); + $rknumlow = self::getInt4d($data, 0); + $sign = ($rknumhigh & self::HIGH_ORDER_BIT) >> 31; + $exp = (($rknumhigh & 0x7FF00000) >> 20) - 1023; + $mantissa = (0x100000 | ($rknumhigh & 0x000FFFFF)); + $mantissalow1 = ($rknumlow & self::HIGH_ORDER_BIT) >> 31; + $mantissalow2 = ($rknumlow & 0x7FFFFFFF); + $value = $mantissa / 2 ** (20 - $exp); + + if ($mantissalow1 != 0) { + $value += 1 / 2 ** (21 - $exp); + } + + if ($mantissalow2 != 0) { + $value += $mantissalow2 / 2 ** (52 - $exp); + } + if ($sign) { + $value *= -1; + } + + return $value; + } + + protected static function getIEEE754(int $rknum): float|int + { + if (($rknum & 0x02) != 0) { + $value = $rknum >> 2; + } else { + // changes by mmp, info on IEEE754 encoding from + // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html + // The RK format calls for using only the most significant 30 bits + // of the 64 bit floating point value. The other 34 bits are assumed + // to be 0 so we use the upper 30 bits of $rknum as follows... + $sign = ($rknum & self::HIGH_ORDER_BIT) >> 31; + $exp = ($rknum & 0x7FF00000) >> 20; + $mantissa = (0x100000 | ($rknum & 0x000FFFFC)); + $value = $mantissa / 2 ** (20 - ($exp - 1023)); + if ($sign) { + $value = -1 * $value; + } + //end of changes by mmp + } + if (($rknum & 0x01) != 0) { + $value /= 100; + } + + return $value; + } + + /** + * Get UTF-8 string from (compressed or uncompressed) UTF-16 string. + */ + protected static function encodeUTF16(string $string, bool $compressed = false): string + { + if ($compressed) { + $string = self::uncompressByteString($string); + } + + return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE'); + } + + /** + * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8. + */ + protected static function uncompressByteString(string $string): string + { + $uncompressedString = ''; + $strLen = strlen($string); + for ($i = 0; $i < $strLen; ++$i) { + $uncompressedString .= $string[$i] . "\0"; + } + + return $uncompressedString; + } + + /** + * Convert string to UTF-8. Only used for BIFF5. + */ + protected function decodeCodepage(string $string): string + { + return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage); + } + + protected static function confirmPos(string $data, int $pos): void + { + if ($pos >= strlen($data)) { + throw new PhpSpreadsheetException('File appears to be corrupt'); // @codeCoverageIgnore + } + } + + /** + * Read 16-bit unsigned integer. + */ + public static function getUInt2d(string $data, int $pos): int + { + self::confirmPos($data, $pos + 1); + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8); + } + + /** + * Read 16-bit signed integer. + */ + public static function getInt2d(string $data, int $pos): int + { + self::confirmPos($data, $pos + 1); + + return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore-line + } + + /** + * Read 32-bit signed integer. + */ + public static function getInt4d(string $data, int $pos): int + { + self::confirmPos($data, $pos + 3); + + // FIX: represent numbers correctly on 64-bit system + // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 + // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems + $_or_24 = ord($data[$pos + 3]); + if ($_or_24 >= 128) { + // negative number + $_ord_24 = -abs((256 - $_or_24) << 24); + } else { + $_ord_24 = ($_or_24 & 127) << 24; + } + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx.php new file mode 100644 index 0000000..2ae91dd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx.php @@ -0,0 +1,2633 @@ +parseHuge = $parseHuge; + } + + /** + * Create a new Xlsx Reader instance. + */ + public function __construct() + { + parent::__construct(); + $this->referenceHelper = ReferenceHelper::getInstance(); + $this->securityScanner = XmlScanner::getInstance($this); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) { + return false; + } + + $result = false; + $this->zip = $zip = new ZipArchive(); + + if ($zip->open($filename) === true) { + [$workbookBasename] = $this->getWorkbookBaseName(); + $result = !empty($workbookBasename); + + $zip->close(); + } + + return $result; + } + + public static function testSimpleXml(mixed $value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement(''); + } + + public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement + { + return self::testSimpleXml($value === null ? $value : $value->attributes($ns)); + } + + // Phpstan thinks, correctly, that xpath can return false. + /** @return mixed[] */ + private static function xpathNoFalse(SimpleXMLElement $sxml, string $path): array + { + return self::falseToArray($sxml->xpath($path)); + } + + /** @return mixed[] */ + public static function falseToArray(mixed $value): array + { + return is_array($value) ? $value : []; + } + + private function loadZip(string $filename, string $ns = '', bool $replaceUnclosedBr = false): SimpleXMLElement + { + $contents = $this->getFromZipArchive($this->zip, $filename); + if ($replaceUnclosedBr) { + $contents = str_replace('
', '
', $contents); + } + $rels = @simplexml_load_string( + $this->getSecurityScannerOrThrow()->scan($contents), + SimpleXMLElement::class, + $this->parseHuge ? LIBXML_PARSEHUGE : 0, + $ns + ); + + return self::testSimpleXml($rels); + } + + // This function is just to identify cases where I'm not sure + // why empty namespace is required. + private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement + { + $contents = $this->getFromZipArchive($this->zip, $filename); + $rels = simplexml_load_string( + $this->getSecurityScannerOrThrow()->scan($contents), + SimpleXMLElement::class, + $this->parseHuge ? LIBXML_PARSEHUGE : 0, + ($ns === '' ? $ns : '') + ); + + return self::testSimpleXml($rels); + } + + private const REL_TO_MAIN = [ + Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN, + Namespaces::THUMBNAIL => '', + ]; + + private const REL_TO_DRAWING = [ + Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING, + ]; + + private const REL_TO_CHART = [ + Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART, + ]; + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. + * + * @return string[] + */ + public function listWorksheetNames(string $filename): array + { + File::assertFile($filename, self::INITIAL_FILE); + + $worksheetNames = []; + + $this->zip = $zip = new ZipArchive(); + $zip->open($filename); + + // The files we're looking at here are small enough that simpleXML is more efficient than XMLReader + $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); + foreach ($rels->Relationship as $relx) { + $rel = self::getAttributes($relx); + $relType = (string) $rel['Type']; + $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN; + if ($mainNS !== '') { + $xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS); + + if ($xmlWorkbook->sheets) { + foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { + // Check if sheet should be skipped + $worksheetNames[] = (string) self::getAttributes($eleSheet)['name']; + } + } + } + } + + $zip->close(); + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + File::assertFile($filename, self::INITIAL_FILE); + + $worksheetInfo = []; + + $this->zip = $zip = new ZipArchive(); + $zip->open($filename); + + $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); + foreach ($rels->Relationship as $relx) { + $rel = self::getAttributes($relx); + $relType = (string) $rel['Type']; + $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN; + if ($mainNS !== '') { + $relTarget = (string) $rel['Target']; + $dir = dirname($relTarget); + $namespace = dirname($relType); + $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS); + + $worksheets = []; + foreach ($relsWorkbook->Relationship as $elex) { + $ele = self::getAttributes($elex); + if ( + ((string) $ele['Type'] === "$namespace/worksheet") + || ((string) $ele['Type'] === "$namespace/chartsheet") + ) { + $worksheets[(string) $ele['Id']] = $ele['Target']; + } + } + + $xmlWorkbook = $this->loadZip($relTarget, $mainNS); + if ($xmlWorkbook->sheets) { + $dir = dirname($relTarget); + + foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { + $tmpInfo = [ + 'worksheetName' => (string) self::getAttributes($eleSheet)['name'], + 'lastColumnLetter' => 'A', + 'lastColumnIndex' => 0, + 'totalRows' => 0, + 'totalColumns' => 0, + ]; + $sheetState = (string) (self::getAttributes($eleSheet)['state'] ?? Worksheet::SHEETSTATE_VISIBLE); + $tmpInfo['sheetState'] = $sheetState; + + $fileWorksheet = (string) $worksheets[self::getArrayItemString(self::getAttributes($eleSheet, $namespace), 'id')]; + $fileWorksheetPath = str_starts_with($fileWorksheet, '/') ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet"; + + $xml = new XMLReader(); + $xml->xml( + $this->getSecurityScannerOrThrow() + ->scan( + $this->getFromZipArchive( + $this->zip, + $fileWorksheetPath + ) + ), + null, + $this->parseHuge ? LIBXML_PARSEHUGE : 0 + ); + $xml->setParserProperty(2, true); + + $currCells = 0; + while ($xml->read()) { + if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) { + $row = (int) $xml->getAttribute('r'); + $tmpInfo['totalRows'] = $row; + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $currCells = 0; + } elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) { + $cell = $xml->getAttribute('r'); + $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1); + } + } + $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); + $xml->close(); + + $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + + $worksheetInfo[] = $tmpInfo; + } + } + } + } + + $zip->close(); + + return $worksheetInfo; + } + + private static function castToBoolean(SimpleXMLElement $c): bool + { + $value = isset($c->v) ? (string) $c->v : null; + if ($value == '0') { + return false; + } elseif ($value == '1') { + return true; + } + + return (bool) $c->v; + } + + private static function castToError(?SimpleXMLElement $c): ?string + { + return isset($c, $c->v) ? (string) $c->v : null; + } + + private static function castToString(?SimpleXMLElement $c): ?string + { + return isset($c, $c->v) ? (string) $c->v : null; + } + + public static function replacePrefixes(string $formula): string + { + return str_replace(['_xlfn.', '_xlws.'], '', $formula); + } + + private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void + { + if ($c === null) { + return; + } + $attr = $c->f->attributes(); + $cellDataType = DataType::TYPE_FORMULA; + $formula = self::replacePrefixes((string) $c->f); + $value = "=$formula"; + $calculatedValue = self::$castBaseType($c); + + // Shared formula? + if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') { + $instance = (string) $attr['si']; + + if (!isset($this->sharedFormulae[(string) $attr['si']])) { + $this->sharedFormulae[$instance] = new SharedFormula($r, $value); + } elseif ($updateSharedCells === true) { + // It's only worth the overhead of adjusting the shared formula for this cell if we're actually loading + // the cell, which may not be the case if we're using a read filter. + $master = Coordinate::indexesFromString($this->sharedFormulae[$instance]->master()); + $current = Coordinate::indexesFromString($r); + + $difference = [0, 0]; + $difference[0] = $current[0] - $master[0]; + $difference[1] = $current[1] - $master[1]; + + $value = $this->referenceHelper->updateFormulaReferences($this->sharedFormulae[$instance]->formula(), 'A1', $difference[0], $difference[1]); + } + } + } + + private function fileExistsInArchive(ZipArchive $archive, string $fileName = ''): bool + { + // Root-relative paths + if (str_contains($fileName, '//')) { + $fileName = substr($fileName, strpos($fileName, '//') + 1); + } + $fileName = File::realpath($fileName); + + // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming + // so we need to load case-insensitively from the zip file + + // Apache POI fixes + $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE); + if ($contents === false) { + $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE); + } + + return $contents !== false; + } + + private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string + { + // Root-relative paths + if (str_contains($fileName, '//')) { + $fileName = substr($fileName, strpos($fileName, '//') + 1); + } + // Relative paths generated by dirname($filename) when $filename + // has no path (i.e.files in root of the zip archive) + $fileName = (string) preg_replace('/^\.\//', '', $fileName); + $fileName = File::realpath($fileName); + + // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming + // so we need to load case-insensitively from the zip file + + $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE); + + // Apache POI fixes + if ($contents === false) { + $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); + } + + // Has the file been saved with Windoze directory separators rather than unix? + if ($contents === false) { + $contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE); + } + + return ($contents === false) ? '' : $contents; + } + + /** + * Loads Spreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + File::assertFile($filename, self::INITIAL_FILE); + + // Initialisations + $excel = $this->newSpreadsheet(); + $excel->setValueBinder($this->valueBinder); + $excel->removeSheetByIndex(0); + $addingFirstCellStyleXf = true; + $addingFirstCellXf = true; + + /** @var mixed[][][][] */ + $unparsedLoadedData = []; + + $this->zip = $zip = new ZipArchive(); + $zip->open($filename); + + // Read the theme first, because we need the colour scheme when reading the styles + [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); + $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; + $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; + $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS); + $theme = null; + $this->styleReader = new Styles(); + foreach ($wbRels->Relationship as $relx) { + $rel = self::getAttributes($relx); + $relTarget = (string) $rel['Target']; + if (str_starts_with($relTarget, '/xl/')) { + $relTarget = substr($relTarget, 4); + } + switch ($rel['Type']) { + case "$xmlNamespaceBase/sheetMetadata": + if ($this->fileExistsInArchive($zip, "xl/{$relTarget}")) { + $excel->returnArrayAsArray(); + } + + break; + case "$xmlNamespaceBase/theme": + if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) { + break; // issue3770 + } + $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; + $themeOrderAdditional = count($themeOrderArray); + + $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS); + $xmlThemeName = self::getAttributes($xmlTheme); + $xmlTheme = $xmlTheme->children($drawingNS); + $themeName = (string) $xmlThemeName['name']; + + $colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme); + $colourSchemeName = (string) $colourScheme['name']; + $excel->getTheme()->setThemeColorName($colourSchemeName); + $colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS); + + $themeColours = []; + foreach ($colourScheme as $k => $xmlColour) { + $themePos = array_search($k, $themeOrderArray); + if ($themePos === false) { + $themePos = $themeOrderAdditional++; + } + if (isset($xmlColour->sysClr)) { + $xmlColourData = self::getAttributes($xmlColour->sysClr); + $themeColours[$themePos] = (string) $xmlColourData['lastClr']; + $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['lastClr']); + } elseif (isset($xmlColour->srgbClr)) { + $xmlColourData = self::getAttributes($xmlColour->srgbClr); + $themeColours[$themePos] = (string) $xmlColourData['val']; + $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['val']); + } + } + $theme = new Theme($themeName, $colourSchemeName, $themeColours); + $this->styleReader->setTheme($theme); + + $fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme); + $fontSchemeName = (string) $fontScheme['name']; + $excel->getTheme()->setThemeFontName($fontSchemeName); + $majorFonts = []; + $minorFonts = []; + $fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS); + $majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? ''; + $majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? ''; + $majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? ''; + $minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? ''; + $minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? ''; + $minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? ''; + + foreach ($fontScheme->majorFont->font as $xmlFont) { + $fontAttributes = self::getAttributes($xmlFont); + $script = (string) ($fontAttributes['script'] ?? ''); + if (!empty($script)) { + $majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? ''); + } + } + foreach ($fontScheme->minorFont->font as $xmlFont) { + $fontAttributes = self::getAttributes($xmlFont); + $script = (string) ($fontAttributes['script'] ?? ''); + if (!empty($script)) { + $minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? ''); + } + } + $excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts); + $excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts); + + break; + } + } + + $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); + + $propertyReader = new PropertyReader($this->getSecurityScannerOrThrow(), $excel->getProperties()); + $charts = $chartDetails = []; + foreach ($rels->Relationship as $relx) { + $rel = self::getAttributes($relx); + $relTarget = (string) $rel['Target']; + // issue 3553 + if ($relTarget[0] === '/') { + $relTarget = substr($relTarget, 1); + } + $relType = (string) $rel['Type']; + $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN; + switch ($relType) { + case Namespaces::CORE_PROPERTIES: + $propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget)); + + break; + case "$xmlNamespaceBase/extended-properties": + $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget)); + + break; + case "$xmlNamespaceBase/custom-properties": + $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); + + break; + //Ribbon + case Namespaces::EXTENSIBILITY: + $customUI = $relTarget; + if ($customUI) { + $this->readRibbon($excel, $customUI, $zip); + } + + break; + case "$xmlNamespaceBase/officeDocument": + $dir = dirname($relTarget); + + // Do not specify namespace in next stmt - do it in Xpath + $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS); + $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS); + + $worksheets = []; + $macros = $customUI = null; + foreach ($relsWorkbook->Relationship as $elex) { + $ele = self::getAttributes($elex); + switch ($ele['Type']) { + case Namespaces::WORKSHEET: + case Namespaces::PURL_WORKSHEET: + $worksheets[(string) $ele['Id']] = $ele['Target']; + + break; + case Namespaces::CHARTSHEET: + if ($this->includeCharts === true) { + $worksheets[(string) $ele['Id']] = $ele['Target']; + } + + break; + // a vbaProject ? (: some macros) + case Namespaces::VBA: + $macros = $ele['Target']; + + break; + } + } + + if ($macros !== null) { + $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin + if (!empty($macrosCode)) { + $excel->setMacrosCode($macrosCode); + $excel->setHasMacros(true); + //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir + $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin'); + $excel->setMacrosCertificate($Certificate); + } + } + + $relType = "rel:Relationship[@Type='" + . "$xmlNamespaceBase/styles" + . "']"; + /** @var ?SimpleXMLElement */ + $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType)); + + if ($xpath === null) { + $xmlStyles = self::testSimpleXml(null); + } else { + $stylesTarget = (string) $xpath['Target']; + $stylesTarget = str_starts_with($stylesTarget, '/') ? substr($stylesTarget, 1) : "$dir/$stylesTarget"; + $xmlStyles = $this->loadZip($stylesTarget, $mainNS); + } + + $palette = self::extractPalette($xmlStyles); + $this->styleReader->setWorkbookPalette($palette); + $fills = self::extractStyles($xmlStyles, 'fills', 'fill'); + $fonts = self::extractStyles($xmlStyles, 'fonts', 'font'); + $borders = self::extractStyles($xmlStyles, 'borders', 'border'); + $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf'); + $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf'); + + $styles = []; + $cellStyles = []; + $numFmts = null; + if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) { + $numFmts = $xmlStyles->numFmts[0]; + } + if (isset($numFmts)) { + /** @var SimpleXMLElement $numFmts */ + $numFmts->registerXPathNamespace('sml', $mainNS); + } + $this->styleReader->setNamespace($mainNS); + if (!$this->readDataOnly/* && $xmlStyles*/) { + foreach ($xfTags as $xfTag) { + /** @var SimpleXMLElement $xfTag */ + $xf = self::getAttributes($xfTag); + $numFmt = null; + + if ($xf['numFmtId']) { + if (isset($numFmts)) { + /** @var ?SimpleXMLElement */ + $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); + + if (isset($tmpNumFmt['formatCode'])) { + $numFmt = (string) $tmpNumFmt['formatCode']; + } + } + + // We shouldn't override any of the built-in MS Excel values (values below id 164) + // But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used + // So we make allowance for them rather than lose formatting masks + if ( + $numFmt === null + && (int) $xf['numFmtId'] < 164 + && NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== '' + ) { + $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); + } + } + $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? ''); + + $style = (object) [ + 'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL, + 'font' => $fonts[(int) ($xf['fontId'])], + 'fill' => $fills[(int) ($xf['fillId'])], + 'border' => $borders[(int) ($xf['borderId'])], + 'alignment' => $xfTag->alignment, + 'protection' => $xfTag->protection, + 'quotePrefix' => $quotePrefix, + ]; + $styles[] = $style; + + // add style to cellXf collection + $objStyle = new Style(); + $this->styleReader + ->readStyle($objStyle, $style); + foreach ($this->styleReader->getFontCharsets() as $fontName => $charset) { + $excel->addFontCharset($fontName, $charset); + } + if ($addingFirstCellXf) { + $excel->removeCellXfByIndex(0); // remove the default style + $addingFirstCellXf = false; + } + $excel->addCellXf($objStyle); + } + + foreach ($cellXfTags as $xfTag) { + /** @var SimpleXMLElement $xfTag */ + $xf = self::getAttributes($xfTag); + $numFmt = NumberFormat::FORMAT_GENERAL; + if ($numFmts && $xf['numFmtId']) { + /** @var ?SimpleXMLElement */ + $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); + if (isset($tmpNumFmt['formatCode'])) { + $numFmt = (string) $tmpNumFmt['formatCode']; + } elseif ((int) $xf['numFmtId'] < 165) { + $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); + } + } + + $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? ''); + + $cellStyle = (object) [ + 'numFmt' => $numFmt, + 'font' => $fonts[(int) ($xf['fontId'])], + 'fill' => $fills[((int) $xf['fillId'])], + 'border' => $borders[(int) ($xf['borderId'])], + 'alignment' => $xfTag->alignment, + 'protection' => $xfTag->protection, + 'quotePrefix' => $quotePrefix, + ]; + $cellStyles[] = $cellStyle; + + // add style to cellStyleXf collection + $objStyle = new Style(); + $this->styleReader->readStyle($objStyle, $cellStyle); + if ($addingFirstCellStyleXf) { + $excel->removeCellStyleXfByIndex(0); // remove the default style + $addingFirstCellStyleXf = false; + } + $excel->addCellStyleXf($objStyle); + } + } + $this->styleReader->setStyleXml($xmlStyles); + $this->styleReader->setNamespace($mainNS); + $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); + $dxfs = $this->styleReader->dxfs($this->readDataOnly); + $tableStyles = $this->styleReader->tableStyles($this->readDataOnly); + $styles = $this->styleReader->styles(); + + // Read content after setting the styles + $sharedStrings = []; + $relType = "rel:Relationship[@Type='" + //. Namespaces::SHARED_STRINGS + . "$xmlNamespaceBase/sharedStrings" + . "']"; + /** @var ?SimpleXMLElement */ + $xpath = self::getArrayItem($relsWorkbook->xpath($relType)); + + if ($xpath) { + $sharedStringsTarget = (string) $xpath['Target']; + $sharedStringsTarget = str_starts_with($sharedStringsTarget, '/') ? substr($sharedStringsTarget, 1) : "$dir/$sharedStringsTarget"; + $xmlStrings = $this->loadZip($sharedStringsTarget, $mainNS); + if (isset($xmlStrings->si)) { + foreach ($xmlStrings->si as $val) { + if (isset($val->t)) { + $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t); + } elseif (isset($val->r)) { + $sharedStrings[] = $this->parseRichText($val); + } else { + $sharedStrings[] = ''; + } + } + } + } + + $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS); + $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS); + + // Set base date + $excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + if ($xmlWorkbookNS->workbookPr) { + Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr); + if (isset($attrs1904['date1904'])) { + if (self::boolean((string) $attrs1904['date1904'])) { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + $excel->setExcelCalendar(Date::CALENDAR_MAC_1904); + } + } + } + + // Set protection + $this->readProtection($excel, $xmlWorkbook); + + $sheetId = 0; // keep track of new sheet id in final workbook + $oldSheetId = -1; // keep track of old sheet id in final workbook + $countSkippedSheets = 0; // keep track of number of skipped sheets + $mapSheetId = []; // mapping of sheet ids from old to new + + $charts = $chartDetails = []; + + $sheetCreated = false; + if ($xmlWorkbookNS->sheets) { + foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) { + $eleSheetAttr = self::getAttributes($eleSheet); + ++$oldSheetId; + + // Check if sheet should be skipped + if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) { + ++$countSkippedSheets; + $mapSheetId[$oldSheetId] = null; + + continue; + } + + $sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); + if (isset($worksheets[$sheetReferenceId]) === false) { + ++$countSkippedSheets; + $mapSheetId[$oldSheetId] = null; + + continue; + } + // Map old sheet id in original workbook to new sheet id. + // They will differ if loadSheetsOnly() is being used + $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets; + + // Load sheet + $docSheet = $excel->createSheet(); + $sheetCreated = true; + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet + // references in formula cells... during the load, all formulae should be correct, + // and we're simply bringing the worksheet name in line with the formula, not the + // reverse + $docSheet->setTitle((string) $eleSheetAttr['name'], false, false); + + $fileWorksheet = (string) $worksheets[$sheetReferenceId]; + // issue 3665 adds test for /. + // This broke XlsxRootZipFilesTest, + // but Excel reports an error with that file. + // Testing dir for . avoids this problem. + // It might be better just to drop the test. + if ($fileWorksheet[0] == '/' && $dir !== '.') { + $fileWorksheet = substr($fileWorksheet, strlen($dir) + 2); + } + $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS); + $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS); + + // Shared Formula table is unique to each Worksheet, so we need to reset it here + $this->sharedFormulae = []; + + if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') { + $docSheet->setSheetState((string) $eleSheetAttr['state']); + } + if ($xmlSheetNS) { + $xmlSheetMain = $xmlSheetNS->children($mainNS); + // Setting Conditional Styles adjusts selected cells, so we need to execute this + // before reading the sheet view data to get the actual selected cells + if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) { + (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load(); + } + if (!$this->readDataOnly && $xmlSheet->extLst) { + (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt(); + } + if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) { + $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet); + $sheetViews->load(); + } + + $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS); + $sheetViewOptions->load($this->readDataOnly, $this->styleReader); + + (new ColumnAndRowAttributes($docSheet, $xmlSheetNS)) + ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells); + } + + $holdSelectedCells = $docSheet->getSelectedCells(); + if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) { + $cIndex = 1; // Cell Start from 1 + foreach ($xmlSheetNS->sheetData->row as $row) { + $rowIndex = 1; + foreach ($row->c as $c) { + $cAttr = self::getAttributes($c); + $r = (string) $cAttr['r']; + if ($r == '') { + $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex; + } + $cellDataType = (string) $cAttr['t']; + $originalCellDataTypeNumeric = $cellDataType === ''; + $value = null; + $calculatedValue = null; + + // Read cell? + $coordinates = Coordinate::coordinateFromString($r); + + if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) { + // Normally, just testing for the f attribute should identify this cell as containing a formula + // that we need to read, even though it is outside of the filter range, in case it is a shared formula. + // But in some cases, this attribute isn't set; so we need to delve a level deeper and look at + // whether or not the cell has a child formula element that is shared. + if (isset($cAttr->f) || (isset($c->f, $c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')) { + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError', false); + } + ++$rowIndex; + + continue; + } + + // Read cell! + $useFormula = isset($c->f) + && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')); + switch ($cellDataType) { + case DataType::TYPE_STRING: + if ((string) $c->v != '') { + $value = $sharedStrings[(int) ($c->v)]; + + if ($value instanceof RichText) { + $value = clone $value; + } + } else { + $value = ''; + } + + break; + case DataType::TYPE_BOOL: + if (!$useFormula) { + if (isset($c->v)) { + $value = self::castToBoolean($c); + } else { + $value = null; + $cellDataType = DataType::TYPE_NULL; + } + } else { + // Formula + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean'); + self::storeFormulaAttributes($c->f, $docSheet, $r); + } + + break; + case DataType::TYPE_STRING2: + if ($useFormula) { + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString'); + self::storeFormulaAttributes($c->f, $docSheet, $r); + } else { + $value = self::castToString($c); + } + + break; + case DataType::TYPE_INLINE: + if ($useFormula) { + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError'); + self::storeFormulaAttributes($c->f, $docSheet, $r); + } else { + $value = $this->parseRichText($c->is); + } + + break; + case DataType::TYPE_ERROR: + if (!$useFormula) { + $value = self::castToError($c); + } else { + // Formula + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError'); + $eattr = $c->attributes(); + if (isset($eattr['vm'])) { + if ($calculatedValue === ExcelError::VALUE()) { + $calculatedValue = ExcelError::SPILL(); + } + } + } + + break; + default: + if (!$useFormula) { + $value = self::castToString($c); + if (is_numeric($value)) { + $value += 0; + $cellDataType = DataType::TYPE_NUMERIC; + } + } else { + // Formula + $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString'); + if (is_numeric($calculatedValue)) { + $calculatedValue += 0; + } + self::storeFormulaAttributes($c->f, $docSheet, $r); + } + + break; + } + + // read empty cells or the cells are not empty + if ($this->readEmptyCells || ($value !== null && $value !== '')) { + // Rich text? + if ($value instanceof RichText && $this->readDataOnly) { + $value = $value->getPlainText(); + } + + $cell = $docSheet->getCell($r); + // Assign value + if ($cellDataType != '') { + // it is possible, that datatype is numeric but with an empty string, which result in an error + if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) { + $cellDataType = DataType::TYPE_NULL; + } + if ($cellDataType !== DataType::TYPE_NULL) { + $cell->setValueExplicit($value, $cellDataType); + } + } else { + $cell->setValue($value); + } + if ($calculatedValue !== null) { + $cell->setCalculatedValue($calculatedValue, $originalCellDataTypeNumeric); + } + + // Style information? + if (!$this->readDataOnly) { + $cAttrS = (int) ($cAttr['s'] ?? 0); + // no style index means 0, it seems + $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0; + $cell->setXfIndex($cAttrS); + // issue 3495 + if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore-line + $holdSelected = $docSheet->getSelectedCells(); + $cell->getStyle()->setQuotePrefix(false); + $docSheet->setSelectedCells($holdSelected); + } + } + } + ++$rowIndex; + } + ++$cIndex; + } + } + $docSheet->setSelectedCells($holdSelectedCells); + if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->ignoredErrors) { + foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) { + $this->processIgnoredErrors($ignoredError, $docSheet); + } + } + + if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) { + $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? []; + foreach ($protAttr as $key => $value) { + $method = 'set' . ucfirst($key); + $docSheet->getProtection()->$method(self::boolean((string) $value)); + } + } + + if ($xmlSheet) { + $this->readSheetProtection($docSheet, $xmlSheet); + } + + if ($this->readDataOnly === false) { + $this->readAutoFilter($xmlSheetNS, $docSheet); + $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'); + } + + $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs); + + if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) { + foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) { + $mergeCell = $mergeCellx->attributes(); + $mergeRef = (string) ($mergeCell['ref'] ?? ''); + if (str_contains($mergeRef, ':')) { + $docSheet->mergeCells($mergeRef, Worksheet::MERGE_CELL_CONTENT_HIDE); + } + } + } + + if ($xmlSheet && !$this->readDataOnly) { + $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData); + } + + if (isset($xmlSheet->extLst->ext)) { + foreach ($xmlSheet->extLst->ext as $extlst) { + $extAttrs = $extlst->attributes() ?? []; + $extUri = (string) ($extAttrs['uri'] ?? ''); + if ($extUri !== '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}') { + continue; + } + // Create dataValidations node if does not exists, maybe is better inside the foreach ? + if (!$xmlSheet->dataValidations) { + $xmlSheet->addChild('dataValidations'); + } + + foreach ($extlst->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) { + $item = self::testSimpleXml($item); + $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation'); + foreach ($item->attributes() ?? [] as $attr) { + $node->addAttribute($attr->getName(), $attr); + } + $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref); + if (isset($item->formula1)) { + $childNode = $node->addChild('formula1'); + if ($childNode !== null) { // null should never happen + // see https://github.com/phpstan/phpstan/issues/8236 + // resolved with Phpstan 2.1.23 + $childNode[0] = (string) $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f; + } + } + } + } + } + + if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) { + (new DataValidations($docSheet, $xmlSheet))->load(); + } + + // unparsed sheet AlternateContent + if ($xmlSheet && !$this->readDataOnly) { + $mc = $xmlSheet->children(Namespaces::COMPATIBILITY); + if ($mc->AlternateContent) { + foreach ($mc->AlternateContent as $alternateContent) { + $alternateContent = self::testSimpleXml($alternateContent); + /** @var mixed[][][][] $unparsedLoadedData */ + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML(); + } + } + } + + // Add hyperlinks + if (!$this->readDataOnly) { + $hyperlinkReader = new Hyperlinks($docSheet); + // Locate hyperlink relations + $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; + if ($zip->locateName($relationsFileName) !== false) { + $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS); + $hyperlinkReader->readHyperlinks($relsWorksheet); + } + + // Loop through hyperlinks + if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) { + $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks); + } + } + + // Add comments + $comments = []; + $vmlComments = []; + if (!$this->readDataOnly) { + // Locate comment relations + $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; + if ($zip->locateName($commentRelations) !== false) { + $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS); + foreach ($relsWorksheet->Relationship as $elex) { + $ele = self::getAttributes($elex); + if ($ele['Type'] == Namespaces::COMMENTS) { + $comments[(string) $ele['Id']] = (string) $ele['Target']; + } + if ($ele['Type'] == Namespaces::VML) { + $vmlComments[(string) $ele['Id']] = (string) $ele['Target']; + } + } + } + + // Loop through comments + foreach ($comments as $relName => $relPath) { + // Load comments file + $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath); + // okay to ignore namespace - using xpath + $commentsFile = $this->loadZip($relPath, ''); + + // Utility variables + $authors = []; + $commentsFile->registerXpathNamespace('com', $mainNS); + $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author'); + foreach ($authorPath as $author) { + /** @var SimpleXMLElement $author */ + $authors[] = (string) $author; + } + + // Loop through contents + $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment'); + foreach ($contentPath as $comment) { + /** @var SimpleXMLElement $comment */ + $commentx = $comment->attributes(); + /** @var array{ref: scalar, authorId?: scalar} $commentx */ + $commentModel = $docSheet->getComment((string) $commentx['ref']); + if (isset($commentx['authorId'])) { + $commentModel->setAuthor($authors[(int) $commentx['authorId']]); + } + /** @var SimpleXMLElement */ + $temp = $comment->children($mainNS); + $commentModel->setText($this->parseRichText($temp->text)); + } + } + + // later we will remove from it real vmlComments + $unparsedVmlDrawings = $vmlComments; + $vmlDrawingContents = []; + + // Loop through VML comments + foreach ($vmlComments as $relName => $relPath) { + // Load VML comments file + $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath); + + try { + // no namespace okay - processed with Xpath + $vmlCommentsFile = $this->loadZip($relPath, '', true); + $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML); + } catch (Throwable) { + //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData + continue; + } + + // Locate VML drawings image relations + $drowingImages = []; + $VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels'; + $vmlDrawingContents[$relName] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $relPath)); + if ($zip->locateName($VMLDrawingsRelations) !== false) { + $relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS); + foreach ($relsVMLDrawing->Relationship as $elex) { + $ele = self::getAttributes($elex); + if ($ele['Type'] == Namespaces::IMAGE) { + $drowingImages[(string) $ele['Id']] = (string) $ele['Target']; + } + } + } + + $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape'); + foreach ($shapes as $shape) { + /** @var SimpleXMLElement $shape */ + $vmlNamespaces = $shape->getNamespaces(); + $shape->registerXPathNamespace('v', $vmlNamespaces['v'] ?? Namespaces::URN_VML); + $shape->registerXPathNamespace('x', $vmlNamespaces['x'] ?? Namespaces::URN_EXCEL); + $shape->registerXPathNamespace('o', $vmlNamespaces['o'] ?? Namespaces::URN_MSOFFICE); + + if (isset($shape['style'])) { + $style = (string) $shape['style']; + $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1)); + $column = null; + $row = null; + $textHAlign = null; + $fillImageRelId = null; + $fillImageTitle = ''; + + $clientData = $shape->xpath('.//x:ClientData'); + $textboxDirection = ''; + $textboxPath = $shape->xpath('.//v:textbox'); + $textbox = (string) ($textboxPath[0]['style'] ?? ''); + if (preg_match('/rtl/i', $textbox) === 1) { + $textboxDirection = Comment::TEXTBOX_DIRECTION_RTL; + } elseif (preg_match('/ltr/i', $textbox) === 1) { + $textboxDirection = Comment::TEXTBOX_DIRECTION_LTR; + } + if (is_array($clientData) && !empty($clientData)) { + /** @var SimpleXMLElement */ + $clientData = $clientData[0]; + + if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') { + $clientData->registerXPathNamespace('x', $vmlNamespaces['x'] ?? Namespaces::URN_EXCEL); + $temp = $clientData->xpath('.//x:Row'); + if (is_array($temp)) { + $row = $temp[0]; + } + + $temp = $clientData->xpath('.//x:Column'); + if (is_array($temp)) { + $column = $temp[0]; + } + $temp = $clientData->xpath('.//x:TextHAlign'); + if (!empty($temp)) { + $textHAlign = strtolower($temp[0]); + } + } + } + $rowx = (string) $row; + $colx = (string) $column; + if (is_numeric($rowx) && is_numeric($colx) && $textHAlign !== null) { + $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setAlignment((string) $textHAlign); + } + if (is_numeric($rowx) && is_numeric($colx) && $textboxDirection !== '') { + $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setTextboxDirection($textboxDirection); + } + + $fillImageRelNode = $shape->xpath('.//v:fill/@o:relid'); + if (is_array($fillImageRelNode) && !empty($fillImageRelNode)) { + /** @var SimpleXMLElement */ + $fillImageRelNode = $fillImageRelNode[0]; + + if (isset($fillImageRelNode['relid'])) { + $fillImageRelId = (string) $fillImageRelNode['relid']; + } + } + + $fillImageTitleNode = $shape->xpath('.//v:fill/@o:title'); + if (is_array($fillImageTitleNode) && !empty($fillImageTitleNode)) { + /** @var SimpleXMLElement */ + $fillImageTitleNode = $fillImageTitleNode[0]; + + if (isset($fillImageTitleNode['title'])) { + $fillImageTitle = (string) $fillImageTitleNode['title']; + } + } + + if (($column !== null) && ($row !== null)) { + // Set comment properties + $comment = $docSheet->getComment([(int) $column + 1, (int) $row + 1]); + $comment->getFillColor()->setRGB($fillColor); + if (isset($fillImageRelId, $drowingImages[$fillImageRelId])) { + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + $objDrawing->setName($fillImageTitle); + $imagePath = str_replace(['../', '/xl/'], 'xl/', $drowingImages[$fillImageRelId]); + $objDrawing->setPath( + 'zip://' . File::realpath($filename) . '#' . $imagePath, + true, + $zip + ); + $comment->setBackgroundImage($objDrawing); + } + + // Parse style + $styleArray = explode(';', str_replace(' ', '', $style)); + foreach ($styleArray as $stylePair) { + $stylePair = explode(':', $stylePair); + + if ($stylePair[0] == 'margin-left') { + $comment->setMarginLeft($stylePair[1]); + } + if ($stylePair[0] == 'margin-top') { + $comment->setMarginTop($stylePair[1]); + } + if ($stylePair[0] == 'width') { + $comment->setWidth($stylePair[1]); + } + if ($stylePair[0] == 'height') { + $comment->setHeight($stylePair[1]); + } + if ($stylePair[0] == 'visibility') { + $comment->setVisible($stylePair[1] == 'visible'); + } + } + + unset($unparsedVmlDrawings[$relName]); + } + } + } + } + + // unparsed vmlDrawing + if ($unparsedVmlDrawings) { + foreach ($unparsedVmlDrawings as $rId => $relPath) { + /** @var mixed[][][] $unparsedLoadedData */ + $rId = substr($rId, 3); // rIdXXX + /** @var mixed[][] */ + $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings']; + $unparsedVmlDrawing[$rId] = []; + $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath); + $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath; + $unparsedVmlDrawing[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath'])); + unset($unparsedVmlDrawing); + } + } + + // Header/footer images + if ($xmlSheetNS && $xmlSheetNS->legacyDrawingHF) { + $vmlHfRid = ''; + $vmlHfRidAttr = $xmlSheetNS->legacyDrawingHF->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT); + if ($vmlHfRidAttr !== null && isset($vmlHfRidAttr['id'])) { + $vmlHfRid = (string) $vmlHfRidAttr['id'][0]; + } + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') !== false) { + $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS); + $vmlRelationship = ''; + + foreach ($relsWorksheet->Relationship as $ele) { + if ((string) $ele['Type'] == Namespaces::VML && (string) $ele['Id'] === $vmlHfRid) { + $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + + break; + } + } + + if ($vmlRelationship != '') { + // Fetch linked images + $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS); + $drawings = []; + if (isset($relsVML->Relationship)) { + foreach ($relsVML->Relationship as $ele) { + if ($ele['Type'] == Namespaces::IMAGE) { + $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']); + } + } + } + // Fetch VML document + $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, ''); + $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML); + + $hfImages = []; + + $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape'); + foreach ($shapes as $idx => $shape) { + /** @var SimpleXMLElement $shape */ + $shape->registerXPathNamespace('v', Namespaces::URN_VML); + $imageData = $shape->xpath('//v:imagedata'); + + if (empty($imageData)) { + continue; + } + + $imageData = $imageData[$idx]; + + $imageData = self::getAttributes($imageData, Namespaces::URN_MSOFFICE); + /** @var array{width: int, height: int, margin-left?: int, margin-top: int} */ + $style = self::toCSSArray((string) $shape['style']); + + if (array_key_exists((string) $imageData['relid'], $drawings)) { + $shapeId = (string) $shape['id']; + $hfImages[$shapeId] = new HeaderFooterDrawing(); + if (isset($imageData['title'])) { + $hfImages[$shapeId]->setName((string) $imageData['title']); + } + + $hfImages[$shapeId]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false, $zip); + $hfImages[$shapeId]->setResizeProportional(false); + $hfImages[$shapeId]->setWidth($style['width']); + $hfImages[$shapeId]->setHeight($style['height']); + if (isset($style['margin-left'])) { + $hfImages[$shapeId]->setOffsetX($style['margin-left']); + } + $hfImages[$shapeId]->setOffsetY($style['margin-top']); + $hfImages[$shapeId]->setResizeProportional(true); + } + } + + $docSheet->getHeaderFooter()->setImages($hfImages); + } + } + } + } + + // TODO: Autoshapes from twoCellAnchors! + $drawingFilename = dirname("$dir/$fileWorksheet") + . '/_rels/' + . basename($fileWorksheet) + . '.rels'; + if (str_starts_with($drawingFilename, 'xl//xl/')) { + $drawingFilename = substr($drawingFilename, 4); + } + if (str_starts_with($drawingFilename, '/xl//xl/')) { + $drawingFilename = substr($drawingFilename, 5); + } + if ($zip->locateName($drawingFilename) !== false) { + $relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS); + $drawings = []; + foreach ($relsWorksheet->Relationship as $elex) { + $ele = self::getAttributes($elex); + if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { + $eleTarget = (string) $ele['Target']; + if (str_starts_with($eleTarget, '/xl/')) { + $drawings[(string) $ele['Id']] = substr($eleTarget, 1); + } else { + $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + } + } + } + + if ($xmlSheetNS->drawing && !$this->readDataOnly) { + $unparsedDrawings = []; + $fileDrawing = null; + foreach ($xmlSheetNS->drawing as $drawing) { + $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); + $fileDrawing = $drawings[$drawingRelId]; + $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels'; + $relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS); + + $images = []; + $hyperlinks = []; + if ($relsDrawing && $relsDrawing->Relationship) { + foreach ($relsDrawing->Relationship as $elex) { + $ele = self::getAttributes($elex); + $eleType = (string) $ele['Type']; + if ($eleType === Namespaces::HYPERLINK) { + $hyperlinks[(string) $ele['Id']] = (string) $ele['Target']; + } + if ($eleType === "$xmlNamespaceBase/image") { + $eleTarget = (string) $ele['Target']; + if (str_starts_with($eleTarget, '/xl/')) { + $eleTarget = substr($eleTarget, 1); + $images[(string) $ele['Id']] = $eleTarget; + } else { + $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $eleTarget); + } + } elseif ($eleType === "$xmlNamespaceBase/chart") { + if ($this->includeCharts) { + $eleTarget = (string) $ele['Target']; + if (str_starts_with($eleTarget, '/xl/')) { + $index = substr($eleTarget, 1); + } else { + $index = self::dirAdd($fileDrawing, $eleTarget); + } + $charts[$index] = [ + 'id' => (string) $ele['Id'], + 'sheet' => $docSheet->getTitle(), + ]; + } + } + } + } + + $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, ''); + $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING); + + if ($xmlDrawingChildren->oneCellAnchor) { + foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) { + $oneCellAnchor = self::testSimpleXml($oneCellAnchor); + if ($oneCellAnchor->pic->blipFill) { + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; + if (isset($blip, $blip->alphaModFix)) { + $temp = (string) $blip->alphaModFix->attributes()->amt; + if (is_numeric($temp)) { + $objDrawing->setOpacity((int) $temp); + } + } + $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; + $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; + + $objDrawing->setName(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name')); + $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr')); + $embedImageKey = self::getArrayItemString( + self::getAttributes($blip, $xmlNamespaceBase), + 'embed' + ); + if (isset($images[$embedImageKey])) { + $objDrawing->setPath( + 'zip://' . File::realpath($filename) . '#' + . $images[$embedImageKey], + false, + $zip + ); + } else { + $linkImageKey = self::getArrayItemString( + $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + 'link' + ); + if (isset($images[$linkImageKey])) { + $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); + $objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages); + } + if ($objDrawing->getPath() === '') { + continue; + } + } + $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1)); + + $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff)); + $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff)); + $objDrawing->setResizeProportional(false); + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy'))); + if ($xfrm) { + $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); + $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); + } + if ($outerShdw) { + $shadow = $objDrawing->getShadow(); + $shadow->setVisible(true); + $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad'))); + $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist'))); + $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir'))); + $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn')); + $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr; + $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val')); + if ($clr->alpha) { + $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val')); + if (is_numeric($alpha)) { + $alpha = (int) ($alpha / 1000); + $shadow->setAlpha($alpha); + } + } + } + + $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks); + + $objDrawing->setWorksheet($docSheet); + } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) { + // Exported XLSX from Google Sheets positions charts with a oneCellAnchor + $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); + $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff); + $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff); + $width = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx')); + $height = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy')); + + $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; + $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; + $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => $coordinates, + 'fromOffsetX' => $offsetX, + 'fromOffsetY' => $offsetY, + 'width' => $width, + 'height' => $height, + 'worksheetTitle' => $docSheet->getTitle(), + 'oneCellAnchor' => true, + ]; + } + } + } + if ($xmlDrawingChildren->twoCellAnchor) { + foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) { + $twoCellAnchor = self::testSimpleXml($twoCellAnchor); + if ($twoCellAnchor->pic->blipFill) { + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; + if (isset($blip, $blip->alphaModFix)) { + $temp = (string) $blip->alphaModFix->attributes()->amt; + if (is_numeric($temp)) { + $objDrawing->setOpacity((int) $temp); + } + } + if (isset($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect)) { + $objDrawing->setSrcRect($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect->attributes()); + } + $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; + $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; + $editAs = $twoCellAnchor->attributes(); + if (isset($editAs, $editAs['editAs'])) { + $objDrawing->setEditAs($editAs['editAs']); + } + $objDrawing->setName((string) self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); + $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); + $embedImageKey = self::getArrayItemString( + self::getAttributes($blip, $xmlNamespaceBase), + 'embed' + ); + if (isset($images[$embedImageKey])) { + $objDrawing->setPath( + 'zip://' . File::realpath($filename) . '#' + . $images[$embedImageKey], + false, + $zip + ); + } else { + $linkImageKey = self::getArrayItemString( + $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + 'link' + ); + if (isset($images[$linkImageKey])) { + $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); + $objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages); + } + if ($objDrawing->getPath() === '') { + continue; + } + } + $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1)); + + $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff)); + $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff)); + + $objDrawing->setCoordinates2(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1)); + + $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff)); + $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff)); + + $objDrawing->setResizeProportional(false); + + if ($xfrm) { + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cy'))); + $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot'))); + $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV')); + $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH')); + } + if ($outerShdw) { + $shadow = $objDrawing->getShadow(); + $shadow->setVisible(true); + $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad'))); + $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist'))); + $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir'))); + $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn')); + $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr; + $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val')); + if ($clr->alpha) { + $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val')); + if (is_numeric($alpha)) { + $alpha = (int) ($alpha / 1000); + $shadow->setAlpha($alpha); + } + } + } + + $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks); + + $objDrawing->setWorksheet($docSheet); + } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) { + $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1); + $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff); + $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff); + $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1); + $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff); + $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff); + $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; + $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; + $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => $fromCoordinate, + 'fromOffsetX' => $fromOffsetX, + 'fromOffsetY' => $fromOffsetY, + 'toCoordinate' => $toCoordinate, + 'toOffsetX' => $toOffsetX, + 'toOffsetY' => $toOffsetY, + 'worksheetTitle' => $docSheet->getTitle(), + ]; + } + } + } + if ($xmlDrawingChildren->absoluteAnchor) { + foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) { + if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) { + $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; + $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; + $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); + $width = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cx')[0]); + $height = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cy')[0]); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => 'A1', + 'fromOffsetX' => 0, + 'fromOffsetY' => 0, + 'width' => $width, + 'height' => $height, + 'worksheetTitle' => $docSheet->getTitle(), + ]; + } + } + } + if (empty($relsDrawing) && $xmlDrawing->count() == 0) { + // Save Drawing without rels and children as unparsed + $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML(); + } + } + + // store original rId of drawing files + /** @var mixed[][][][] $unparsedLoadedData */ + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = []; + foreach ($relsWorksheet->Relationship as $elex) { + $ele = self::getAttributes($elex); + if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { + $drawingRelId = (string) $ele['Id']; + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId; + if (isset($unparsedDrawings[$drawingRelId])) { + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId]; + } + } + } + if ($xmlSheet->legacyDrawing && !$this->readDataOnly) { + foreach ($xmlSheet->legacyDrawing as $drawing) { + $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); + if (isset($vmlDrawingContents[$drawingRelId])) { + if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) { + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId]; + } + } + } + } + + // unparsed drawing AlternateContent + $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY); + + if ($xmlAltDrawing->AlternateContent) { + foreach ($xmlAltDrawing->AlternateContent as $alternateContent) { + $alternateContent = self::testSimpleXml($alternateContent); + /** @var mixed[][][][][] $unparsedLoadedData */ + $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML(); + } + } + } + } + + /** @var mixed[][][][] $unparsedLoadedData */ + $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData); + $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData); + + // Loop through definedNames + if ($xmlWorkbook->definedNames) { + foreach ($xmlWorkbook->definedNames->definedName as $definedName) { + // Extract range + $extractedRange = (string) $definedName; + if (($spos = strpos($extractedRange, '!')) !== false) { + $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos)); + } else { + $extractedRange = str_replace('$', '', $extractedRange); + } + + // Valid range? + if ($extractedRange == '') { + continue; + } + + // Some definedNames are only applicable if we are on the same sheet... + if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) { + // Switch on type + switch ((string) $definedName['name']) { + case '_xlnm._FilterDatabase': + if ((string) $definedName['hidden'] !== '1') { + $extractedRange = explode(',', $extractedRange); + foreach ($extractedRange as $range) { + $autoFilterRange = $range; + if (str_contains($autoFilterRange, ':')) { + $docSheet->getAutoFilter()->setRange($autoFilterRange); + } + } + } + + break; + case '_xlnm.Print_Titles': + // Split $extractedRange + $extractedRange = explode(',', $extractedRange); + + // Set print titles + foreach ($extractedRange as $range) { + $matches = []; + $range = str_replace('$', '', $range); + + // check for repeating columns, e g. 'A:A' or 'A:D' + if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) { + $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]); + } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) { + // check for repeating rows, e.g. '1:1' or '1:5' + $docSheet->getPageSetup()->setRowsToRepeatAtTop([(int) $matches[1], (int) $matches[2]]); + } + } + + break; + case '_xlnm.Print_Area': + $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; + $newRangeSets = []; + foreach ($rangeSets as $rangeSet) { + [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + if (empty($rangeSet)) { + continue; + } + if (!str_contains($rangeSet, ':')) { + $rangeSet = $rangeSet . ':' . $rangeSet; + } + $newRangeSets[] = str_replace('$', '', $rangeSet); + } + if (count($newRangeSets) > 0) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + } + + break; + default: + break; + } + } + } + } + + // Next sheet id + ++$sheetId; + } + + // Loop through definedNames + if ($xmlWorkbook->definedNames) { + foreach ($xmlWorkbook->definedNames->definedName as $definedName) { + // Extract range + $extractedRange = (string) $definedName; + + // Valid range? + if ($extractedRange == '') { + continue; + } + + // Some definedNames are only applicable if we are on the same sheet... + if ((string) $definedName['localSheetId'] != '') { + // Local defined name + // Switch on type + switch ((string) $definedName['name']) { + case '_xlnm._FilterDatabase': + case '_xlnm.Print_Titles': + case '_xlnm.Print_Area': + break; + default: + if ($mapSheetId[(int) $definedName['localSheetId']] !== null) { + $range = Worksheet::extractSheetTitle($extractedRange, true); + $scope = $excel->getSheet($mapSheetId[(int) $definedName['localSheetId']]); + if (str_contains((string) $definedName, '!')) { + $range[0] = str_replace("''", "'", $range[0]); + $range[0] = str_replace("'", '', $range[0]); + if ($worksheet = $excel->getSheetByName($range[0])) { + $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); + } else { + $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope)); + } + } else { + $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true)); + } + } + + break; + } + } elseif (!isset($definedName['localSheetId'])) { + // "Global" definedNames + $locatedSheet = null; + if (str_contains((string) $definedName, '!')) { + // Modify range, and extract the first worksheet reference + // Need to split on a comma or a space if not in quotes, and extract the first part. + $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $extractedRange); + if (is_array($definedNameValueParts)) { + // Extract sheet name + [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true, true); + + // Locate sheet + $locatedSheet = $excel->getSheetByName("$extractedSheetName"); + } + } + + if ($locatedSheet === null && !DefinedName::testIfFormula($extractedRange)) { + $extractedRange = '#REF!'; + } + $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false)); + } + } + } + } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $excel->createSheet(); + } + + (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly); + + break; + } + } + + if (!$this->readDataOnly) { + $contentTypes = $this->loadZip('[Content_Types].xml'); + + // Default content types + foreach ($contentTypes->Default as $contentType) { + switch ($contentType['ContentType']) { + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings': + $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType']; + + break; + } + } + + // Override content types + foreach ($contentTypes->Override as $contentType) { + switch ($contentType['ContentType']) { + case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml': + if ($this->includeCharts) { + $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); + $chartElements = $this->loadZip($chartEntryRef); + $chartReader = new Chart($chartNS, $drawingNS); + $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml')); + if (isset($charts[$chartEntryRef])) { + $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; + if (isset($chartDetails[$chartPositionRef]) && $excel->getSheetByName($charts[$chartEntryRef]['sheet']) !== null) { + $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); + $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); + // For oneCellAnchor or absoluteAnchor positioned charts, + // toCoordinate is not in the data. Does it need to be calculated? + if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) { + // twoCellAnchor + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + } else { + // oneCellAnchor or absoluteAnchor (e.g. Chart sheet) + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']); + if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) { + $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']); + } + } + } + } + } + + break; + + // unparsed + case 'application/vnd.ms-excel.controlproperties+xml': + $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; + + break; + } + } + } + + /** @var array|string>>> $unparsedLoadedData */ + $excel->setUnparsedLoadedData($unparsedLoadedData); + + $zip->close(); + + return $excel; + } + + private function parseRichText(?SimpleXMLElement $is): RichText + { + $value = new RichText(); + + if (isset($is->t)) { + $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); + } elseif ($is !== null) { + if (is_object($is->r)) { + foreach ($is->r as $run) { + if (!isset($run->rPr)) { + $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + } else { + $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + $objFont = $objText->getFont() ?? new StyleFont(); + + if (isset($run->rPr->rFont)) { + $attr = $run->rPr->rFont->attributes(); + if (isset($attr['val'])) { + $objFont->setName((string) $attr['val']); + } + } + if (isset($run->rPr->sz)) { + $attr = $run->rPr->sz->attributes(); + if (isset($attr['val'])) { + $objFont->setSize((float) $attr['val']); + } + } + if (isset($run->rPr->color)) { + $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color))); + } + if (isset($run->rPr->b)) { + $attr = $run->rPr->b->attributes(); + if ( + (isset($attr['val']) && self::boolean((string) $attr['val'])) + || (!isset($attr['val'])) + ) { + $objFont->setBold(true); + } + } + if (isset($run->rPr->i)) { + $attr = $run->rPr->i->attributes(); + if ( + (isset($attr['val']) && self::boolean((string) $attr['val'])) + || (!isset($attr['val'])) + ) { + $objFont->setItalic(true); + } + } + if (isset($run->rPr->vertAlign)) { + $attr = $run->rPr->vertAlign->attributes(); + if (isset($attr['val'])) { + $vertAlign = strtolower((string) $attr['val']); + if ($vertAlign == 'superscript') { + $objFont->setSuperscript(true); + } + if ($vertAlign == 'subscript') { + $objFont->setSubscript(true); + } + } + } + if (isset($run->rPr->u)) { + $attr = $run->rPr->u->attributes(); + if (!isset($attr['val'])) { + $objFont->setUnderline(StyleFont::UNDERLINE_SINGLE); + } else { + $objFont->setUnderline((string) $attr['val']); + } + } + if (isset($run->rPr->strike)) { + $attr = $run->rPr->strike->attributes(); + if ( + (isset($attr['val']) && self::boolean((string) $attr['val'])) + || (!isset($attr['val'])) + ) { + $objFont->setStrikethrough(true); + } + } + } + } + } + } + + return $value; + } + + private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchive $zip): void + { + $baseDir = dirname($customUITarget); + $nameCustomUI = basename($customUITarget); + // get the xml file (ribbon) + $localRibbon = $this->getFromZipArchive($zip, $customUITarget); + $customUIImagesNames = []; + $customUIImagesBinaries = []; + // something like customUI/_rels/customUI.xml.rels + $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels'; + $dataRels = $this->getFromZipArchive($zip, $pathRels); + if ($dataRels) { + // exists and not empty if the ribbon have some pictures (other than internal MSO) + $UIRels = simplexml_load_string( + $this->getSecurityScannerOrThrow() + ->scan($dataRels), + SimpleXMLElement::class, + $this->parseHuge ? LIBXML_PARSEHUGE : 0 + ); + if (false !== $UIRels) { + // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image + foreach ($UIRels->Relationship as $ele) { + if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/image') { + // an image ? + $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target']; + $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']); + } + } + } + } + if ($localRibbon) { + $excel->setRibbonXMLData($customUITarget, $localRibbon); + if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) { + $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries); + } else { + $excel->setRibbonBinObjects(null, null); + } + } else { + $excel->setRibbonXMLData(null, null); + $excel->setRibbonBinObjects(null, null); + } + } + + /** @param null|bool|mixed[]|SimpleXMLElement $array */ + private static function getArrayItem(null|array|bool|SimpleXMLElement $array, int|string $key = 0): mixed + { + return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); + } + + /** @param null|bool|mixed[]|SimpleXMLElement $array */ + private static function getArrayItemString(null|array|bool|SimpleXMLElement $array, int|string $key = 0): string + { + $retVal = self::getArrayItem($array, $key); + + return StringHelper::convertToString($retVal, false); + } + + /** @param null|bool|mixed[]|SimpleXMLElement $array */ + private static function getArrayItemIntOrSxml(null|array|bool|SimpleXMLElement $array, int|string $key = 0): int|SimpleXMLElement + { + $retVal = self::getArrayItem($array, $key); + + return (is_int($retVal) || $retVal instanceof SimpleXMLElement) ? $retVal : 0; + } + + private static function dirAdd(null|SimpleXMLElement|string $base, null|SimpleXMLElement|string $add): string + { + $base = (string) $base; + $add = (string) $add; + + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); + } + + /** @return mixed[] */ + private static function toCSSArray(string $style): array + { + $style = self::stripWhiteSpaceFromStyleString($style); + + $temp = explode(';', $style); + $style = []; + foreach ($temp as $item) { + $item = explode(':', $item); + + if (str_contains($item[1], 'px')) { + $item[1] = str_replace('px', '', $item[1]); + } + if (str_contains($item[1], 'pt')) { + $item[1] = str_replace('pt', '', $item[1]); + $item[1] = (string) Font::fontSizeToPixels((int) $item[1]); + } + if (str_contains($item[1], 'in')) { + $item[1] = str_replace('in', '', $item[1]); + $item[1] = (string) Font::inchSizeToPixels((int) $item[1]); + } + if (str_contains($item[1], 'cm')) { + $item[1] = str_replace('cm', '', $item[1]); + $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]); + } + + $style[$item[0]] = $item[1]; + } + + return $style; + } + + public static function stripWhiteSpaceFromStyleString(string $string): string + { + return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); + } + + private static function boolean(string $value): bool + { + if (is_numeric($value)) { + return (bool) $value; + } + + return $value === 'true' || $value === 'TRUE'; + } + + /** @param string[] $hyperlinks */ + private function readHyperLinkDrawing(\PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing, SimpleXMLElement $cellAnchor, array $hyperlinks): void + { + $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; + + if ($hlinkClick->count() === 0) { + return; + } + + $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id']; + $hyperlink = new Hyperlink( + $hyperlinks[$hlinkId], + self::getArrayItemString(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name') + ); + $objDrawing->setHyperlink($hyperlink); + } + + private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void + { + if (!$xmlWorkbook->workbookProtection) { + return; + } + + $security = $excel->getSecurity(); + $security->setLockRevision( + self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision') + ); + $security->setLockStructure( + self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure') + ); + $security->setLockWindows( + self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows') + ); + + if ($xmlWorkbook->workbookProtection['revisionsPassword']) { + $security->setRevisionsPassword( + (string) $xmlWorkbook->workbookProtection['revisionsPassword'], + true + ); + } + if ($xmlWorkbook->workbookProtection['revisionsAlgorithmName']) { + $security->setRevisionsAlgorithmName( + (string) $xmlWorkbook->workbookProtection['revisionsAlgorithmName'] + ); + } + if ($xmlWorkbook->workbookProtection['revisionsSaltValue']) { + $security->setRevisionsSaltValue( + (string) $xmlWorkbook->workbookProtection['revisionsSaltValue'], + false + ); + } + if ($xmlWorkbook->workbookProtection['revisionsSpinCount']) { + $security->setRevisionsSpinCount( + (int) $xmlWorkbook->workbookProtection['revisionsSpinCount'] + ); + } + if ($xmlWorkbook->workbookProtection['revisionsHashValue']) { + if ($security->advancedRevisionsPassword()) { + $security->setRevisionsPassword( + (string) $xmlWorkbook->workbookProtection['revisionsHashValue'], + true + ); + } + } + + if ($xmlWorkbook->workbookProtection['workbookPassword']) { + $security->setWorkbookPassword( + (string) $xmlWorkbook->workbookProtection['workbookPassword'], + true + ); + } + + if ($xmlWorkbook->workbookProtection['workbookAlgorithmName']) { + $security->setWorkbookAlgorithmName( + (string) $xmlWorkbook->workbookProtection['workbookAlgorithmName'] + ); + } + if ($xmlWorkbook->workbookProtection['workbookSaltValue']) { + $security->setWorkbookSaltValue( + (string) $xmlWorkbook->workbookProtection['workbookSaltValue'], + false + ); + } + if ($xmlWorkbook->workbookProtection['workbookSpinCount']) { + $security->setWorkbookSpinCount( + (int) $xmlWorkbook->workbookProtection['workbookSpinCount'] + ); + } + if ($xmlWorkbook->workbookProtection['workbookHashValue']) { + if ($security->advancedPassword()) { + $security->setWorkbookPassword( + (string) $xmlWorkbook->workbookProtection['workbookHashValue'], + true + ); + } + } + } + + private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool + { + $returnValue = null; + $protectKey = $protection[$key]; + if (!empty($protectKey)) { + $protectKey = (string) $protectKey; + $returnValue = $protectKey !== 'false' && (bool) $protectKey; + } + + return $returnValue; + } + + /** @param mixed[][][][] $unparsedLoadedData */ + private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void + { + $zip = $this->zip; + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) { + return; + } + + $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; + $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS); + $ctrlProps = []; + foreach ($relsWorksheet->Relationship as $ele) { + if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') { + $ctrlProps[(string) $ele['Id']] = $ele; + } + } + + /** @var mixed[][] */ + $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps']; + foreach ($ctrlProps as $rId => $ctrlProp) { + $rId = substr($rId, 3); // rIdXXX + $unparsedCtrlProps[$rId] = []; + $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']); + $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target']; + $unparsedCtrlProps[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath'])); + } + unset($unparsedCtrlProps); + } + + /** @param mixed[][][][] $unparsedLoadedData */ + private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void + { + if ($this->readDataOnly) { + return; + } + $zip = $this->zip; + if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) { + return; + } + + $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; + $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS); + $sheetPrinterSettings = []; + foreach ($relsWorksheet->Relationship as $ele) { + if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') { + $sheetPrinterSettings[(string) $ele['Id']] = $ele; + } + } + + /** @var mixed[][] */ + $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings']; + foreach ($sheetPrinterSettings as $rId => $printerSettings) { + $rId = substr($rId, 3); // rIdXXX + if (!str_ends_with($rId, 'ps')) { + $rId = $rId . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing + } + $unparsedPrinterSettings[$rId] = []; + $target = (string) str_replace('/xl/', '../', (string) $printerSettings['Target']); + $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $target); + $unparsedPrinterSettings[$rId]['relFilePath'] = $target; + $unparsedPrinterSettings[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath'])); + } + unset($unparsedPrinterSettings); + } + + /** @return array{string, string} */ + private function getWorkbookBaseName(): array + { + $workbookBasename = ''; + $xmlNamespaceBase = ''; + + // check if it is an OOXML archive + $rels = $this->loadZip(self::INITIAL_FILE); + foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) { + $rel = self::getAttributes($rel); + $type = (string) $rel['Type']; + switch ($type) { + case Namespaces::OFFICE_DOCUMENT: + case Namespaces::PURL_OFFICE_DOCUMENT: + $basename = basename((string) $rel['Target']); + $xmlNamespaceBase = dirname($type); + if (preg_match('/workbook.*\.xml/', $basename)) { + $workbookBasename = $basename; + } + + break; + } + } + + return [$workbookBasename, $xmlNamespaceBase]; + } + + private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void + { + if ($this->readDataOnly || !$xmlSheet->sheetProtection) { + return; + } + + $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName']; + $protection = $docSheet->getProtection(); + $protection->setAlgorithm($algorithmName); + + if ($algorithmName) { + $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true); + $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']); + $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']); + } else { + $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true); + } + + if ($xmlSheet->protectedRanges->protectedRange) { + foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { + $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']); + } + } + } + + private function readAutoFilter( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet + ): void { + if ($xmlSheet && $xmlSheet->autoFilter) { + (new AutoFilter($docSheet, $xmlSheet))->load(); + } + } + + private function readBackgroundImage( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet, + string $relsName + ): void { + if ($xmlSheet && $xmlSheet->picture) { + $id = (string) self::getArrayItemString(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id'); + $rels = $this->loadZip($relsName); + foreach ($rels->Relationship as $rel) { + $attrs = $rel->attributes() ?? []; + $rid = (string) ($attrs['Id'] ?? ''); + $target = (string) ($attrs['Target'] ?? ''); + if ($rid === $id && str_starts_with($target, '..')) { + $target = 'xl' . substr($target, 2); + $content = $this->getFromZipArchive($this->zip, $target); + $docSheet->setBackgroundImage($content); + } + } + } + } + + /** + * @param TableDxfsStyle[] $tableStyles + * @param Style[] $dxfs + */ + private function readTables( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet, + string $dir, + string $fileWorksheet, + ZipArchive $zip, + string $namespaceTable, + array $tableStyles, + array $dxfs + ): void { + if ($xmlSheet && $xmlSheet->tableParts) { + /** @var array{count: scalar} */ + $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0]; + if (((int) $attributes['count']) > 0) { + $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs); + } + } + } + + /** + * @param TableDxfsStyle[] $tableStyles + * @param Style[] $dxfs + */ + private function readTablesInTablesFile( + SimpleXMLElement $xmlSheet, + string $dir, + string $fileWorksheet, + ZipArchive $zip, + Worksheet $docSheet, + string $namespaceTable, + array $tableStyles, + array $dxfs + ): void { + foreach ($xmlSheet->tableParts->tablePart as $tablePart) { + $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT); + $tablePartRel = (string) $relation['id']; + $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; + + if ($zip->locateName($relationsFileName) !== false) { + $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS); + foreach ($relsTableReferences->Relationship as $relationship) { + $relationshipAttributes = self::getAttributes($relationship, ''); + + if ((string) $relationshipAttributes['Id'] === $tablePartRel) { + $relationshipFileName = (string) $relationshipAttributes['Target']; + $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName; + $relationshipFilePath = File::realpath($relationshipFilePath); + + if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { + $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable); + (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs); + } + } + } + } + } + } + + /** @return mixed[] */ + private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array + { + $array = []; + if ($sxml && $sxml->{$node1}->{$node2}) { + /** @var SimpleXMLElement */ + $temp = $sxml->{$node1}->{$node2}; + foreach ($temp as $node) { + $array[] = $node; + } + } + + return $array; + } + + /** @return string[] */ + private static function extractPalette(?SimpleXMLElement $sxml): array + { + $array = []; + if ($sxml && $sxml->colors->indexedColors) { + foreach ($sxml->colors->indexedColors->rgbColor as $node) { + $attr = $node->attributes(); + if (isset($attr['rgb'])) { + $array[] = (string) $attr['rgb']; + } + } + } + + return $array; + } + + private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void + { + $cellCollection = $sheet->getCellCollection(); + $attributes = self::getAttributes($xml); + $sqref = (string) ($attributes['sqref'] ?? ''); + $numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? ''); + $formula = (string) ($attributes['formula'] ?? ''); + $formulaRange = (string) ($attributes['formulaRange'] ?? ''); + $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? ''); + $evalError = (string) ($attributes['evalError'] ?? ''); + if (!empty($sqref)) { + $explodedSqref = explode(' ', $sqref); + $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/'; + foreach ($explodedSqref as $sqref1) { + if (preg_match($pattern1, $sqref1, $matches) === 1) { + $firstRow = $matches[2]; + $firstCol = $matches[1]; + if (array_key_exists(3, $matches)) { + $lastCol = $matches[4]; + $lastRow = $matches[5]; + } else { + $lastCol = $firstCol; + $lastRow = $firstRow; + } + StringHelper::stringIncrement($lastCol); + for ($row = $firstRow; $row <= $lastRow; ++$row) { + for ($col = $firstCol; $col !== $lastCol; StringHelper::stringIncrement($col)) { + if (!$cellCollection->has2("$col$row")) { + continue; + } + if ($numberStoredAsText === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true); + } + if ($formula === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true); + } + if ($formulaRange === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setFormulaRange(true); + } + if ($twoDigitTextYear === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true); + } + if ($evalError === '1') { + $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true); + } + } + } + } + } + } + } + + private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void + { + $formulaAttributes = []; + $attributes = $f->attributes(); + if (isset($attributes['t'])) { + $formulaAttributes['t'] = (string) $attributes['t']; + } + if (isset($attributes['ref'])) { + $formulaAttributes['ref'] = (string) $attributes['ref']; + } + if (!empty($formulaAttributes)) { + $docSheet->getCell($r)->setFormulaAttributes($formulaAttributes); + } + } + + private static function onlyNoteVml(string $data): bool + { + $data = str_replace('
', '
', $data); + + try { + $sxml = @simplexml_load_string($data); + } catch (Throwable) { + $sxml = false; + } + + if ($sxml === false) { + return false; + } + $shapes = $sxml->children(Namespaces::URN_VML); + foreach ($shapes->shape as $shape) { + $clientData = $shape->children(Namespaces::URN_EXCEL); + if (!isset($clientData->ClientData)) { + return false; + } + $attrs = $clientData->ClientData->attributes(); + if (!isset($attrs['ObjectType'])) { + return false; + } + $objectType = (string) $attrs['ObjectType']; + if ($objectType !== 'Note') { + return false; + } + } + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php new file mode 100644 index 0000000..88a0831 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -0,0 +1,163 @@ +parent = $parent; + $this->worksheetXml = $worksheetXml; + } + + public function load(): void + { + // Remove all "$" in the auto filter range + $attrs = $this->worksheetXml->autoFilter->attributes() ?? []; + $autoFilterRange = (string) preg_replace('/\$/', '', $attrs['ref'] ?? ''); + if (str_contains($autoFilterRange, ':')) { + $this->readAutoFilter($autoFilterRange); + } + } + + private function readAutoFilter(string $autoFilterRange): void + { + $autoFilter = $this->parent->getAutoFilter(); + $autoFilter->setRange($autoFilterRange); + + foreach ($this->worksheetXml->autoFilter->filterColumn as $filterColumn) { + $attributes = $filterColumn->attributes() ?? []; + $column = $autoFilter->getColumnByOffset((int) ($attributes['colId'] ?? 0)); + // Check for standard filters + if ($filterColumn->filters) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + $filters = Xlsx::testSimpleXml($filterColumn->filters->attributes()); + if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) { + // Operator is undefined, but always treated as EQUAL + $column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + } + // Standard filters are always an OR join, so no join rule needs to be set + // Entries can be either filter elements + foreach ($filterColumn->filters->filter as $filterRule) { + // Operator is undefined, but always treated as EQUAL + /** @var SimpleXMLElement */ + $attr2 = $filterRule->attributes() ?? ['val' => '']; + $column->createRule()->setRule('', (string) $attr2['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + } + + // Or Date Group elements + $this->readDateRangeAutoFilter($filterColumn->filters, $column); + } + + // Check for custom filters + $this->readCustomAutoFilter($filterColumn, $column); + // Check for dynamic filters + $this->readDynamicAutoFilter($filterColumn, $column); + // Check for dynamic filters + $this->readTopTenAutoFilter($filterColumn, $column); + } + $autoFilter->setEvaluated(true); + } + + private function readDateRangeAutoFilter(SimpleXMLElement $filters, Column $column): void + { + foreach ($filters->dateGroupItem as $dateGroupItemx) { + // Operator is undefined, but always treated as EQUAL + $dateGroupItem = $dateGroupItemx->attributes(); + if ($dateGroupItem !== null) { + $column->createRule()->setRule( + '', + [ + 'year' => (string) $dateGroupItem['year'], + 'month' => (string) $dateGroupItem['month'], + 'day' => (string) $dateGroupItem['day'], + 'hour' => (string) $dateGroupItem['hour'], + 'minute' => (string) $dateGroupItem['minute'], + 'second' => (string) $dateGroupItem['second'], + ], + (string) $dateGroupItem['dateTimeGrouping'] + )->setRuleType(Rule::AUTOFILTER_RULETYPE_DATEGROUP); + } + } + } + + private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void + { + if (isset($filterColumn, $filterColumn->customFilters)) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); + $customFilters = $filterColumn->customFilters; + $attributes = $customFilters->attributes(); + // Custom filters can an AND or an OR join; + // and there should only ever be one or two entries + if ((isset($attributes['and'])) && ((string) $attributes['and'] === '1')) { + $column->setJoin(Column::AUTOFILTER_COLUMN_JOIN_AND); + } + foreach ($customFilters->customFilter as $filterRule) { + /** @var SimpleXMLElement */ + $attr2 = $filterRule->attributes() ?? ['operator' => '', 'val' => '']; + $column->createRule()->setRule( + (string) $attr2['operator'], + (string) $attr2['val'] + )->setRuleType(Rule::AUTOFILTER_RULETYPE_CUSTOMFILTER); + } + } + } + + private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void + { + if (isset($filterColumn, $filterColumn->dynamicFilter)) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + // We should only ever have one dynamic filter + foreach ($filterColumn->dynamicFilter as $filterRule) { + // Operator is undefined, but always treated as EQUAL + $attr2 = $filterRule->attributes() ?? []; + $column->createRule()->setRule( + '', + (string) ($attr2['val'] ?? ''), + (string) ($attr2['type'] ?? '') + )->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + if (isset($attr2['val'])) { + $column->setAttribute('val', (string) $attr2['val']); + } + if (isset($attr2['maxVal'])) { + $column->setAttribute('maxVal', (string) $attr2['maxVal']); + } + } + } + } + + private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void + { + if (isset($filterColumn, $filterColumn->top10)) { + $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); + // We should only ever have one top10 filter + foreach ($filterColumn->top10 as $filterRule) { + $attr2 = $filterRule->attributes() ?? []; + $column->createRule()->setRule( + ( + ((isset($attr2['percent'])) && ((string) $attr2['percent'] === '1')) + ? Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT + : Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE + ), + (string) ($attr2['val'] ?? ''), + ( + ((isset($attr2['top'])) && ((string) $attr2['top'] === '1')) + ? Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP + : Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM + ) + )->setRuleType(Rule::AUTOFILTER_RULETYPE_TOPTENFILTER); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php new file mode 100644 index 0000000..beea6bb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php @@ -0,0 +1,21 @@ +cNamespace = $cNamespace; + $this->aNamespace = $aNamespace; + } + + private static function getAttributeString(SimpleXMLElement $component, string $name): string|null + { + $attributes = $component->attributes(); + if (@isset($attributes[$name])) { + return (string) $attributes[$name]; + } + + return null; + } + + private static function getAttributeInteger(SimpleXMLElement $component, string $name): int|null + { + $attributes = $component->attributes(); + if (@isset($attributes[$name])) { + return (int) $attributes[$name]; + } + + return null; + } + + private static function getAttributeBoolean(SimpleXMLElement $component, string $name): bool|null + { + $attributes = $component->attributes(); + if (@isset($attributes[$name])) { + $value = (string) $attributes[$name]; + + return $value === 'true' || $value === '1'; + } + + return null; + } + + private static function getAttributeFloat(SimpleXMLElement $component, string $name): float|null + { + $attributes = $component->attributes(); + if (@isset($attributes[$name])) { + return (float) $attributes[$name]; + } + + return null; + } + + public function readChart(SimpleXMLElement $chartElements, string $chartName): \PhpOffice\PhpSpreadsheet\Chart\Chart + { + $chartElementsC = $chartElements->children($this->cNamespace); + + $XaxisLabel = $YaxisLabel = $legend = $title = null; + $dispBlanksAs = null; + $plotVisOnly = false; + $plotArea = null; + $rotX = $rotY = $rAngAx = $perspective = null; + $xAxis = new Axis(); + $yAxis = new Axis(); + $autoTitleDeleted = null; + $chartNoFill = false; + $chartBorderLines = null; + $chartFillColor = null; + $gradientArray = []; + $gradientLin = null; + $roundedCorners = false; + $gapWidth = null; + $useUpBars = null; + $useDownBars = null; + $noBorder = false; + foreach ($chartElementsC as $chartElementKey => $chartElement) { + switch ($chartElementKey) { + case 'spPr': + $children = $chartElementsC->spPr->children($this->aNamespace); + if (isset($children->noFill)) { + $chartNoFill = true; + } + if (isset($children->solidFill)) { + $chartFillColor = $this->readColor($children->solidFill); + } + if (isset($children->ln)) { + $chartBorderLines = new GridLines(); + $this->readLineStyle($chartElementsC, $chartBorderLines); + if (isset($children->ln->noFill)) { + $noBorder = true; + } + } + + break; + case 'roundedCorners': + /** @var bool $roundedCorners */ + $roundedCorners = self::getAttributeBoolean($chartElementsC->roundedCorners, 'val'); + + break; + case 'chart': + foreach ($chartElement as $chartDetailsKey => $chartDetails) { + $chartDetails = Xlsx::testSimpleXml($chartDetails); + switch ($chartDetailsKey) { + case 'autoTitleDeleted': + /** @var bool $autoTitleDeleted */ + $autoTitleDeleted = self::getAttributeBoolean($chartElementsC->chart->autoTitleDeleted, 'val'); + + break; + case 'view3D': + $rotX = self::getAttributeInteger($chartDetails->rotX, 'val'); + $rotY = self::getAttributeInteger($chartDetails->rotY, 'val'); + $rAngAx = self::getAttributeInteger($chartDetails->rAngAx, 'val'); + $perspective = self::getAttributeInteger($chartDetails->perspective, 'val'); + + break; + case 'plotArea': + $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; + $plotSeries = $plotAttributes = []; + $catAxRead = false; + $plotNoFill = false; + foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); + switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + $gradient = Xlsx::testSimpleXml($gradient); + /** @var float $pos */ + $pos = self::getAttributeFloat($gradient, 'pos'); + $gradientArray[] = [ + $pos / ChartProperties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = ChartProperties::XmlToAngle((string) self::getAttributeString($possibleNoFill->gradFill->lin, 'ang')); + } + + break; + case 'layout': + $plotAreaLayout = $this->chartLayoutDetails($chartDetail); + + break; + case Axis::AXIS_TYPE_CATEGORY: + case Axis::AXIS_TYPE_DATE: + $catAxRead = true; + if (isset($chartDetail->title)) { + $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); + } + $xAxis->setAxisType($chartDetailKey); + $this->readEffects($chartDetail, $xAxis); + $this->readLineStyle($chartDetail, $xAxis); + if (isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + if (isset($chartDetail->spPr->ln->noFill)) { + $xAxis->setNoFill(true); + } + } + if (isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); + } + $xAxis->setMajorGridlines($majorGridlines); + } + if (isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); + } + $xAxis->setMinorGridlines($minorGridlines); + } + $this->setAxisProperties($chartDetail, $xAxis); + + break; + case Axis::AXIS_TYPE_VALUE: + $whichAxis = null; + $axPos = null; + if (isset($chartDetail->axPos)) { + $axPos = self::getAttributeString($chartDetail->axPos, 'val'); + } + if ($catAxRead) { + $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); + } elseif (!empty($axPos)) { + switch ($axPos) { + case 't': + case 'b': + $whichAxis = $xAxis; + $xAxis->setAxisType($chartDetailKey); + + break; + case 'r': + case 'l': + $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); + + break; + } + } + if (isset($chartDetail->title)) { + $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); + + switch ($axPos) { + case 't': + case 'b': + $XaxisLabel = $axisLabel; + + break; + case 'r': + case 'l': + $YaxisLabel = $axisLabel; + + break; + } + } + $this->readEffects($chartDetail, $whichAxis); + $this->readLineStyle($chartDetail, $whichAxis); + if ($whichAxis !== null && isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + if (isset($sppr->ln->noFill)) { + $whichAxis->setNoFill(true); + } + } + if ($whichAxis !== null && isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); + } + $whichAxis->setMajorGridlines($majorGridlines); + } + if ($whichAxis !== null && isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); + } + $whichAxis->setMinorGridlines($minorGridlines); + } + $this->setAxisProperties($chartDetail, $whichAxis); + + break; + case 'barChart': + case 'bar3DChart': + $barDirection = self::getAttributeString($chartDetail->barDir, 'val'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotDirection("$barDirection"); + $plotSeries[] = $plotSer; + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'lineChart': + case 'line3DChart': + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'areaChart': + case 'area3DChart': + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'doughnutChart': + case 'pieChart': + case 'pie3DChart': + $explosion = self::getAttributeString($chartDetail->ser->explosion, 'val'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$explosion"); + $plotSeries[] = $plotSer; + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'scatterChart': + /** @var string $scatterStyle */ + $scatterStyle = self::getAttributeString($chartDetail->scatterStyle, 'val'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle($scatterStyle); + $plotSeries[] = $plotSer; + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'bubbleChart': + $bubbleScale = self::getAttributeInteger($chartDetail->bubbleScale, 'val'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$bubbleScale"); + $plotSeries[] = $plotSer; + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'radarChart': + /** @var string $radarStyle */ + $radarStyle = self::getAttributeString($chartDetail->radarStyle, 'val'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle($radarStyle); + $plotSeries[] = $plotSer; + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'surfaceChart': + case 'surface3DChart': + $wireFrame = self::getAttributeBoolean($chartDetail->wireframe, 'val'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$wireFrame"); + $plotSeries[] = $plotSer; + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + case 'stockChart': + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + if (isset($chartDetail->upDownBars->gapWidth)) { + $gapWidth = self::getAttributeInteger($chartDetail->upDownBars->gapWidth, 'val'); + } + if (isset($chartDetail->upDownBars->upBars)) { + $useUpBars = true; + } + if (isset($chartDetail->upDownBars->downBars)) { + $useDownBars = true; + } + $plotAttributes = $this->readChartAttributes($chartDetail); + + break; + } + } + if ($plotAreaLayout == null) { + $plotAreaLayout = new Layout(); + } + $plotArea = new PlotArea($plotAreaLayout, $plotSeries); + $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } + if (is_int($gapWidth)) { + $plotArea->setGapWidth($gapWidth); + } + if ($useUpBars === true) { + $plotArea->setUseUpBars(true); + } + if ($useDownBars === true) { + $plotArea->setUseDownBars(true); + } + + break; + case 'plotVisOnly': + $plotVisOnly = (bool) self::getAttributeString($chartDetails, 'val'); + + break; + case 'dispBlanksAs': + $dispBlanksAs = self::getAttributeString($chartDetails, 'val'); + + break; + case 'title': + $title = $this->chartTitle($chartDetails); + + break; + case 'legend': + $legendPos = 'r'; + $legendLayout = null; + $legendOverlay = false; + $legendBorderLines = null; + $legendFillColor = null; + $legendText = null; + $addLegendText = false; + foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); + switch ($chartDetailKey) { + case 'legendPos': + $legendPos = self::getAttributeString($chartDetail, 'val'); + + break; + case 'overlay': + $legendOverlay = self::getAttributeBoolean($chartDetail, 'val'); + + break; + case 'layout': + $legendLayout = $this->chartLayoutDetails($chartDetail); + + break; + case 'spPr': + $children = $chartDetails->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $legendFillColor = $this->readColor($children->solidFill); + } + if (isset($children->ln)) { + $legendBorderLines = new GridLines(); + $this->readLineStyle($chartDetails, $legendBorderLines); + } + + break; + case 'txPr': + $children = $chartDetails->txPr->children($this->aNamespace); + $addLegendText = false; + $legendText = new AxisText(); + if (isset($children->p->pPr->defRPr->solidFill)) { + $colorArray = $this->readColor($children->p->pPr->defRPr->solidFill); + $legendText->getFillColorObject()->setColorPropertiesArray($colorArray); + $addLegendText = true; + } + if (isset($children->p->pPr->defRPr->effectLst)) { + $this->readEffects($children->p->pPr->defRPr, $legendText, false); + $addLegendText = true; + } + + break; + } + } + $legend = new Legend("$legendPos", $legendLayout, (bool) $legendOverlay); + if ($legendFillColor !== null) { + $legend->getFillColor()->setColorPropertiesArray($legendFillColor); + } + if ($legendBorderLines !== null) { + $legend->setBorderLines($legendBorderLines); + } + if ($addLegendText) { + $legend->setLegendText($legendText); + } + + break; + } + } + } + } + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } + if ($chartFillColor !== null) { + $chart->getFillColor()->setColorPropertiesArray($chartFillColor); + } + if ($chartBorderLines !== null) { + $chart->setBorderLines($chartBorderLines); + } + $chart->setNoBorder($noBorder); + $chart->setRoundedCorners($roundedCorners); + if (is_bool($autoTitleDeleted)) { + $chart->setAutoTitleDeleted($autoTitleDeleted); + } + if (is_int($rotX)) { + $chart->setRotX($rotX); + } + if (is_int($rotY)) { + $chart->setRotY($rotY); + } + if (is_int($rAngAx)) { + $chart->setRAngAx($rAngAx); + } + if (is_int($perspective)) { + $chart->setPerspective($perspective); + } + + return $chart; + } + + private function chartTitle(SimpleXMLElement $titleDetails): Title + { + $caption = ''; + $titleLayout = null; + $titleOverlay = false; + $titleFormula = null; + $titleFont = null; + foreach ($titleDetails as $titleDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); + switch ($titleDetailKey) { + case 'tx': + $caption = []; + if (isset($chartDetail->rich)) { + $titleDetails = $chartDetail->rich->children($this->aNamespace); + foreach ($titleDetails as $titleKey => $titleDetail) { + $titleDetail = Xlsx::testSimpleXml($titleDetail); + switch ($titleKey) { + case 'p': + $titleDetailPart = $titleDetail->children($this->aNamespace); + $caption[] = $this->parseRichText($titleDetailPart); + } + } + } elseif (isset($chartDetail->strRef->strCache)) { + foreach ($chartDetail->strRef->strCache->pt as $pt) { + if (isset($pt->v)) { + $caption[] = (string) $pt->v; + } + } + if (isset($chartDetail->strRef->f)) { + $titleFormula = (string) $chartDetail->strRef->f; + } + } + + break; + case 'overlay': + $titleOverlay = self::getAttributeBoolean($chartDetail, 'val'); + + break; + case 'layout': + $titleLayout = $this->chartLayoutDetails($chartDetail); + + break; + case 'txPr': + if (isset($chartDetail->children($this->aNamespace)->p)) { + $titleFont = $this->parseFont($chartDetail->children($this->aNamespace)->p); + } + + break; + } + } + $title = new Title($caption, $titleLayout, (bool) $titleOverlay); + if (!empty($titleFormula)) { + $title->setCellReference($titleFormula); + } + if ($titleFont !== null) { + $title->setFont($titleFont); + } + + return $title; + } + + private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout + { + if (!isset($chartDetail->manualLayout)) { + return null; + } + $details = $chartDetail->manualLayout->children($this->cNamespace); + if ($details === null) { + return null; + } + $layout = []; + foreach ($details as $detailKey => $detail) { + $detail = Xlsx::testSimpleXml($detail); + $layout[$detailKey] = self::getAttributeString($detail, 'val'); + } + + return new Layout($layout); + } + + private function chartDataSeries(SimpleXMLElement $chartDetail, string $plotType): DataSeries + { + $multiSeriesType = null; + $smoothLine = false; + $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = $seriesBubbles = []; + $plotDirection = null; + + $seriesDetailSet = $chartDetail->children($this->cNamespace); + foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) { + switch ($seriesDetailKey) { + case 'grouping': + $multiSeriesType = self::getAttributeString($chartDetail->grouping, 'val'); + + break; + case 'ser': + $marker = null; + $seriesIndex = ''; + $fillColor = null; + $pointSize = null; + $noFill = false; + $bubble3D = false; + $dptColors = []; + $markerFillColor = null; + $markerBorderColor = null; + $lineStyle = null; + $labelLayout = null; + $trendLines = []; + foreach ($seriesDetails as $seriesKey => $seriesDetail) { + $seriesDetail = Xlsx::testSimpleXml($seriesDetail); + switch ($seriesKey) { + case 'idx': + $seriesIndex = self::getAttributeInteger($seriesDetail, 'val'); + + break; + case 'order': + $seriesOrder = self::getAttributeInteger($seriesDetail, 'val'); + if ($seriesOrder !== null) { + $plotOrder[$seriesIndex] = $seriesOrder; + } + + break; + case 'tx': + $temp = $this->chartDataSeriesValueSet($seriesDetail); + if ($temp !== null) { + $seriesLabel[$seriesIndex] = $temp; + } + + break; + case 'spPr': + $children = $seriesDetail->children($this->aNamespace); + if (isset($children->ln)) { + $ln = $children->ln; + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + $lineStyle = new GridLines(); + $this->readLineStyle($seriesDetails, $lineStyle); + } + if (isset($children->effectLst)) { + if ($lineStyle === null) { + $lineStyle = new GridLines(); + } + $this->readEffects($seriesDetails, $lineStyle); + } + if (isset($children->solidFill)) { + $fillColor = new ChartColor($this->readColor($children->solidFill)); + } + + break; + case 'dPt': + $dptIdx = (int) self::getAttributeString($seriesDetail->idx, 'val'); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $arrayColors = $this->readColor($children->solidFill); + $dptColors[$dptIdx] = new ChartColor($arrayColors); + } + } + + break; + case 'trendline': + $trendLine = new TrendLine(); + $this->readLineStyle($seriesDetail, $trendLine); + $trendLineType = self::getAttributeString($seriesDetail->trendlineType, 'val'); + $dispRSqr = self::getAttributeBoolean($seriesDetail->dispRSqr, 'val'); + $dispEq = self::getAttributeBoolean($seriesDetail->dispEq, 'val'); + $order = self::getAttributeInteger($seriesDetail->order, 'val'); + $period = self::getAttributeInteger($seriesDetail->period, 'val'); + $forward = self::getAttributeFloat($seriesDetail->forward, 'val'); + $backward = self::getAttributeFloat($seriesDetail->backward, 'val'); + $intercept = self::getAttributeFloat($seriesDetail->intercept, 'val'); + $name = (string) $seriesDetail->name; + $trendLine->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); + $trendLines[] = $trendLine; + + break; + case 'marker': + $marker = self::getAttributeString($seriesDetail->symbol, 'val'); + $pointSize = self::getAttributeString($seriesDetail->size, 'val'); + $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $markerFillColor = $this->readColor($children->solidFill); + } + if (isset($children->ln->solidFill)) { + $markerBorderColor = $this->readColor($children->ln->solidFill); + } + } + + break; + case 'smooth': + $smoothLine = self::getAttributeBoolean($seriesDetail, 'val') ?? false; + + break; + case 'cat': + $temp = $this->chartDataSeriesValueSet($seriesDetail); + if ($temp !== null) { + $seriesCategory[$seriesIndex] = $temp; + } + + break; + case 'val': + $temp = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); + if ($temp !== null) { + $seriesValues[$seriesIndex] = $temp; + } + + break; + case 'xVal': + $temp = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); + if ($temp !== null) { + $seriesCategory[$seriesIndex] = $temp; + } + + break; + case 'yVal': + $temp = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); + if ($temp !== null) { + $seriesValues[$seriesIndex] = $temp; + } + + break; + case 'bubbleSize': + $seriesBubble = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); + if ($seriesBubble !== null) { + $seriesBubbles[$seriesIndex] = $seriesBubble; + } + + break; + case 'bubble3D': + $bubble3D = self::getAttributeBoolean($seriesDetail, 'val'); + + break; + case 'dLbls': + $labelLayout = new Layout($this->readChartAttributes($seriesDetails)); + + break; + } + } + if ($labelLayout) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLabelLayout($labelLayout); + } + } + if ($noFill) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setScatterLines(false); + } + } + if ($lineStyle !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->copyLineStyles($lineStyle); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->copyLineStyles($lineStyle); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->copyLineStyles($lineStyle); + } + } + if ($bubble3D) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setBubble3D($bubble3D); + } + } + if (!empty($dptColors)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setFillColor($dptColors); + } + } + if ($markerFillColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + } + if ($markerBorderColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + } + if ($smoothLine) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSmoothLine(true); + } + } + if (!empty($trendLines)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setTrendLines($trendLines); + } + } + } + } + $series = new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $plotDirection, $smoothLine); + $series->setPlotBubbleSizes($seriesBubbles); + + return $series; + } + + private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?ChartColor $fillColor = null, ?string $pointSize = null): ?DataSeriesValues + { + if (isset($seriesDetail->strRef)) { + $seriesSource = (string) $seriesDetail->strRef->f; + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); + + if (isset($seriesDetail->strRef->strCache)) { + /** @var array{formatCode: string, dataValues: mixed[]} */ + $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's'); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } + + return $seriesValues; + } elseif (isset($seriesDetail->numRef)) { + $seriesSource = (string) $seriesDetail->numRef->f; + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); + if (isset($seriesDetail->numRef->numCache)) { + /** @var array{formatCode: string, dataValues: mixed[]} */ + $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace)); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } + + return $seriesValues; + } elseif (isset($seriesDetail->multiLvlStrRef)) { + $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); + + if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { + /** @var array{formatCode: string, dataValues: mixed[]} */ + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's'); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } + + return $seriesValues; + } elseif (isset($seriesDetail->multiLvlNumRef)) { + $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); + + if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { + /** @var array{formatCode: string, dataValues: mixed[]} */ + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's'); + $seriesValues + ->setFormatCode($seriesData['formatCode']) + ->setDataValues($seriesData['dataValues']); + } + + return $seriesValues; + } + + if (isset($seriesDetail->v)) { + return new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + null, + null, + 1, + [(string) $seriesDetail->v] + ); + } + + return null; + } + + /** @return mixed[] */ + private function chartDataSeriesValues(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array + { + $seriesVal = []; + $formatCode = ''; + $pointCount = 0; + + foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) { + $seriesValue = Xlsx::testSimpleXml($seriesValue); + switch ($seriesValueIdx) { + case 'ptCount': + $pointCount = self::getAttributeInteger($seriesValue, 'val'); + + break; + case 'formatCode': + $formatCode = (string) $seriesValue; + + break; + case 'pt': + $pointVal = self::getAttributeInteger($seriesValue, 'idx'); + if ($dataType == 's') { + $seriesVal[$pointVal] = (string) $seriesValue->v; + } elseif ((string) $seriesValue->v === ExcelError::NA()) { + $seriesVal[$pointVal] = null; + } else { + $seriesVal[$pointVal] = (float) $seriesValue->v; + } + + break; + } + } + + return [ + 'formatCode' => $formatCode, + 'pointCount' => $pointCount, + 'dataValues' => $seriesVal, + ]; + } + + /** @return mixed[] */ + private function chartDataSeriesValuesMultiLevel(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array + { + $seriesVal = []; + $formatCode = ''; + $pointCount = 0; + + foreach ($seriesValueSet->lvl as $seriesLevelIdx => $seriesLevel) { + foreach ($seriesLevel as $seriesValueIdx => $seriesValue) { + $seriesValue = Xlsx::testSimpleXml($seriesValue); + switch ($seriesValueIdx) { + case 'ptCount': + $pointCount = self::getAttributeInteger($seriesValue, 'val'); + + break; + case 'formatCode': + $formatCode = (string) $seriesValue; + + break; + case 'pt': + $pointVal = self::getAttributeInteger($seriesValue, 'idx'); + if ($dataType == 's') { + $seriesVal[$pointVal][] = (string) $seriesValue->v; + } elseif ((string) $seriesValue->v === ExcelError::NA()) { + $seriesVal[$pointVal] = null; + } else { + $seriesVal[$pointVal][] = (float) $seriesValue->v; + } + + break; + } + } + } + + return [ + 'formatCode' => $formatCode, + 'pointCount' => $pointCount, + 'dataValues' => $seriesVal, + ]; + } + + private function parseRichText(SimpleXMLElement $titleDetailPart): RichText + { + $value = new RichText(); + $defaultFontSize = null; + $defaultBold = null; + $defaultItalic = null; + $defaultUnderscore = null; + $defaultStrikethrough = null; + $defaultBaseline = null; + $defaultFontName = null; + $defaultLatin = null; + $defaultEastAsian = null; + $defaultComplexScript = null; + $defaultFontColor = null; + if (isset($titleDetailPart->pPr->defRPr)) { + $defaultFontSize = self::getAttributeInteger($titleDetailPart->pPr->defRPr, 'sz'); + $defaultBold = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'b'); + $defaultItalic = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i'); + $defaultUnderscore = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u'); + $defaultStrikethrough = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike'); + $defaultBaseline = self::getAttributeInteger($titleDetailPart->pPr->defRPr, 'baseline'); + if (isset($titleDetailPart->defRPr->rFont['val'])) { + $defaultFontName = (string) $titleDetailPart->defRPr->rFont['val']; + } + if (isset($titleDetailPart->pPr->defRPr->latin)) { + $defaultLatin = self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface'); + } + if (isset($titleDetailPart->pPr->defRPr->ea)) { + $defaultEastAsian = self::getAttributeString($titleDetailPart->pPr->defRPr->ea, 'typeface'); + } + if (isset($titleDetailPart->pPr->defRPr->cs)) { + $defaultComplexScript = self::getAttributeString($titleDetailPart->pPr->defRPr->cs, 'typeface'); + } + if (isset($titleDetailPart->pPr->defRPr->solidFill)) { + $defaultFontColor = $this->readColor($titleDetailPart->pPr->defRPr->solidFill); + } + } + foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { + if ( + (string) $titleDetailElementKey !== 'r' + || !isset($titleDetailElement->t) + ) { + continue; + } + $objText = $value->createTextRun((string) $titleDetailElement->t); + if ($objText->getFont() === null) { + // @codeCoverageIgnoreStart + continue; + // @codeCoverageIgnoreEnd + } + $fontSize = null; + $bold = null; + $italic = null; + $underscore = null; + $strikethrough = null; + $baseline = null; + $fontName = null; + $latinName = null; + $eastAsian = null; + $complexScript = null; + $fontColor = null; + $underlineColor = null; + if (isset($titleDetailElement->rPr)) { + // not used now, not sure it ever was, grandfathering + if (isset($titleDetailElement->rPr->rFont['val'])) { + // @codeCoverageIgnoreStart + $fontName = (string) $titleDetailElement->rPr->rFont['val']; + // @codeCoverageIgnoreEnd + } + if (isset($titleDetailElement->rPr->latin)) { + $latinName = self::getAttributeString($titleDetailElement->rPr->latin, 'typeface'); + } + if (isset($titleDetailElement->rPr->ea)) { + $eastAsian = self::getAttributeString($titleDetailElement->rPr->ea, 'typeface'); + } + if (isset($titleDetailElement->rPr->cs)) { + $complexScript = self::getAttributeString($titleDetailElement->rPr->cs, 'typeface'); + } + $fontSize = self::getAttributeInteger($titleDetailElement->rPr, 'sz'); + + // not used now, not sure it ever was, grandfathering + if (isset($titleDetailElement->rPr->solidFill)) { + $fontColor = $this->readColor($titleDetailElement->rPr->solidFill); + } + + $bold = self::getAttributeBoolean($titleDetailElement->rPr, 'b'); + $italic = self::getAttributeBoolean($titleDetailElement->rPr, 'i'); + $baseline = self::getAttributeInteger($titleDetailElement->rPr, 'baseline'); + $underscore = self::getAttributeString($titleDetailElement->rPr, 'u'); + if (isset($titleDetailElement->rPr->uFill->solidFill)) { + $underlineColor = $this->readColor($titleDetailElement->rPr->uFill->solidFill); + } + + $strikethrough = self::getAttributeString($titleDetailElement->rPr, 'strike'); + } + + $fontFound = false; + $latinName = $latinName ?? $defaultLatin; + if ($latinName !== null) { + $objText->getFont()->setLatin($latinName); + $fontFound = true; + } + $eastAsian = $eastAsian ?? $defaultEastAsian; + if ($eastAsian !== null) { + $objText->getFont()->setEastAsian($eastAsian); + $fontFound = true; + } + $complexScript = $complexScript ?? $defaultComplexScript; + if ($complexScript !== null) { + $objText->getFont()->setComplexScript($complexScript); + $fontFound = true; + } + $fontName = $fontName ?? $defaultFontName; + if ($fontName !== null) { + // @codeCoverageIgnoreStart + $objText->getFont()->setName($fontName); + $fontFound = true; + // @codeCoverageIgnoreEnd + } + + $fontSize = $fontSize ?? $defaultFontSize; + if (is_int($fontSize)) { + $objText->getFont()->setSize(floor($fontSize / 100)); + $fontFound = true; + } else { + $objText->getFont()->setSize(null, true); + } + + $fontColor = $fontColor ?? $defaultFontColor; + if (!empty($fontColor)) { + $objText->getFont()->setChartColor($fontColor); + $fontFound = true; + } + + $bold = $bold ?? $defaultBold; + if ($bold !== null) { + $objText->getFont()->setBold($bold); + $fontFound = true; + } + + $italic = $italic ?? $defaultItalic; + if ($italic !== null) { + $objText->getFont()->setItalic($italic); + $fontFound = true; + } + + $baseline = $baseline ?? $defaultBaseline; + if ($baseline !== null) { + $objText->getFont()->setBaseLine($baseline); + if ($baseline > 0) { + $objText->getFont()->setSuperscript(true); + } elseif ($baseline < 0) { + $objText->getFont()->setSubscript(true); + } + $fontFound = true; + } + + $underscore = $underscore ?? $defaultUnderscore; + if ($underscore !== null) { + if ($underscore == 'sng') { + $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } elseif ($underscore == 'dbl') { + $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } elseif ($underscore !== '') { + $objText->getFont()->setUnderline($underscore); + } else { + $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); + } + $fontFound = true; + if ($underlineColor) { + $objText->getFont()->setUnderlineColor($underlineColor); + } + } + + $strikethrough = $strikethrough ?? $defaultStrikethrough; + if ($strikethrough !== null) { + $objText->getFont()->setStrikeType($strikethrough); + if ($strikethrough == 'noStrike') { + $objText->getFont()->setStrikethrough(false); + } else { + $objText->getFont()->setStrikethrough(true); + } + $fontFound = true; + } + if ($fontFound === false) { + $objText->setFont(null); + } + } + + return $value; + } + + private function parseFont(SimpleXMLElement $titleDetailPart): ?Font + { + if (!isset($titleDetailPart->pPr->defRPr)) { + return null; + } + $fontArray = []; + $fontArray['size'] = self::getAttributeInteger($titleDetailPart->pPr->defRPr, 'sz'); + if ($fontArray['size'] !== null && $fontArray['size'] >= 100) { + $fontArray['size'] /= 100.0; + } + if ($fontArray['size'] !== null) { + $fontArray['size'] = (int) ($fontArray['size']); + } + $fontArray['bold'] = (bool) self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'b'); + $fontArray['italic'] = (bool) self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i'); + $fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u'); + $strikethrough = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike'); + if ($strikethrough !== null) { + if ($strikethrough == 'noStrike') { + $fontArray['strikethrough'] = false; + } else { + $fontArray['strikethrough'] = true; + } + } + $fontArray['cap'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap'); + + if (isset($titleDetailPart->pPr->defRPr->latin)) { + $fontArray['latin'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface'); + } + if (isset($titleDetailPart->pPr->defRPr->ea)) { + $fontArray['eastAsian'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr->ea, 'typeface'); + } + if (isset($titleDetailPart->pPr->defRPr->cs)) { + $fontArray['complexScript'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr->cs, 'typeface'); + } + if (isset($titleDetailPart->pPr->defRPr->solidFill)) { + $fontArray['chartColor'] = new ChartColor($this->readColor($titleDetailPart->pPr->defRPr->solidFill)); + } + $font = new Font(); + //$font->setSize(null, true); + $font->applyFromArray($fontArray); + + return $font; + } + + /** @return mixed[] */ + private function readChartAttributes(?SimpleXMLElement $chartDetail): array + { + $plotAttributes = []; + if (isset($chartDetail->dLbls)) { + if (isset($chartDetail->dLbls->dLblPos)) { + $plotAttributes['dLblPos'] = self::getAttributeString($chartDetail->dLbls->dLblPos, 'val'); + } + if (isset($chartDetail->dLbls->numFmt)) { + $plotAttributes['numFmtCode'] = self::getAttributeString($chartDetail->dLbls->numFmt, 'formatCode'); + $plotAttributes['numFmtLinked'] = self::getAttributeBoolean($chartDetail->dLbls->numFmt, 'sourceLinked'); + } + if (isset($chartDetail->dLbls->showLegendKey)) { + $plotAttributes['showLegendKey'] = self::getAttributeString($chartDetail->dLbls->showLegendKey, 'val'); + } + if (isset($chartDetail->dLbls->showVal)) { + $plotAttributes['showVal'] = self::getAttributeString($chartDetail->dLbls->showVal, 'val'); + } + if (isset($chartDetail->dLbls->showCatName)) { + $plotAttributes['showCatName'] = self::getAttributeString($chartDetail->dLbls->showCatName, 'val'); + } + if (isset($chartDetail->dLbls->showSerName)) { + $plotAttributes['showSerName'] = self::getAttributeString($chartDetail->dLbls->showSerName, 'val'); + } + if (isset($chartDetail->dLbls->showPercent)) { + $plotAttributes['showPercent'] = self::getAttributeString($chartDetail->dLbls->showPercent, 'val'); + } + if (isset($chartDetail->dLbls->showBubbleSize)) { + $plotAttributes['showBubbleSize'] = self::getAttributeString($chartDetail->dLbls->showBubbleSize, 'val'); + } + if (isset($chartDetail->dLbls->showLeaderLines)) { + $plotAttributes['showLeaderLines'] = self::getAttributeString($chartDetail->dLbls->showLeaderLines, 'val'); + } + if (isset($chartDetail->dLbls->spPr)) { + $sppr = $chartDetail->dLbls->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $plotAttributes['labelFillColor'] = new ChartColor($this->readColor($sppr->solidFill)); + } + if (isset($sppr->ln->solidFill)) { + $plotAttributes['labelBorderColor'] = new ChartColor($this->readColor($sppr->ln->solidFill)); + } + } + if (isset($chartDetail->dLbls->txPr)) { + $txpr = $chartDetail->dLbls->txPr->children($this->aNamespace); + if (isset($txpr->p)) { + $plotAttributes['labelFont'] = $this->parseFont($txpr->p); + if (isset($txpr->p->pPr->defRPr->effectLst)) { + $labelEffects = new GridLines(); + $this->readEffects($txpr->p->pPr->defRPr, $labelEffects, false); + $plotAttributes['labelEffects'] = $labelEffects; + } + } + } + } + + return $plotAttributes; + } + + /** @param array $plotAttributes */ + private function setChartAttributes(Layout $plotArea, array $plotAttributes): void + { + foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) { + /** @var ?bool $plotAttributeValue */ + switch ($plotAttributeKey) { + case 'showLegendKey': + $plotArea->setShowLegendKey($plotAttributeValue); + + break; + case 'showVal': + $plotArea->setShowVal($plotAttributeValue); + + break; + case 'showCatName': + $plotArea->setShowCatName($plotAttributeValue); + + break; + case 'showSerName': + $plotArea->setShowSerName($plotAttributeValue); + + break; + case 'showPercent': + $plotArea->setShowPercent($plotAttributeValue); + + break; + case 'showBubbleSize': + $plotArea->setShowBubbleSize($plotAttributeValue); + + break; + case 'showLeaderLines': + $plotArea->setShowLeaderLines($plotAttributeValue); + + break; + case 'labelFont': + /** @var ?Font $plotAttributeValue */ + $plotArea->setLabelFont($plotAttributeValue); + + break; + } + } + } + + private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject, bool $getSppr = true): void + { + if (!isset($chartObject)) { + return; + } + if ($getSppr) { + if (!isset($chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + } else { + $sppr = $chartDetail; + } + if (isset($sppr->effectLst->glow)) { + $axisGlowSize = (float) self::getAttributeInteger($sppr->effectLst->glow, 'rad') / ChartProperties::POINTS_WIDTH_MULTIPLIER; + if ($axisGlowSize != 0.0) { + $colorArray = $this->readColor($sppr->effectLst->glow); + $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']); + } + } + + if (isset($sppr->effectLst->softEdge)) { + $softEdgeSize = self::getAttributeString($sppr->effectLst->softEdge, 'rad'); + if (is_numeric($softEdgeSize)) { + $chartObject->setSoftEdges((float) ChartProperties::xmlToPoints($softEdgeSize)); + } + } + + $type = ''; + foreach (self::SHADOW_TYPES as $shadowType) { + if (isset($sppr->effectLst->$shadowType)) { + $type = $shadowType; + + break; + } + } + if ($type !== '') { + $blur = self::getAttributeString($sppr->effectLst->$type, 'blurRad'); + $blur = is_numeric($blur) ? ChartProperties::xmlToPoints($blur) : null; + $dist = self::getAttributeString($sppr->effectLst->$type, 'dist'); + $dist = is_numeric($dist) ? ChartProperties::xmlToPoints($dist) : null; + $direction = self::getAttributeString($sppr->effectLst->$type, 'dir'); + $direction = is_numeric($direction) ? ChartProperties::xmlToAngle($direction) : null; + $algn = self::getAttributeString($sppr->effectLst->$type, 'algn'); + $rot = self::getAttributeString($sppr->effectLst->$type, 'rotWithShape'); + $size = []; + foreach (['sx', 'sy'] as $sizeType) { + $sizeValue = self::getAttributeString($sppr->effectLst->$type, $sizeType); + if (is_numeric($sizeValue)) { + $size[$sizeType] = ChartProperties::xmlToTenthOfPercent((string) $sizeValue); + } else { + $size[$sizeType] = null; + } + } + foreach (['kx', 'ky'] as $sizeType) { + $sizeValue = self::getAttributeString($sppr->effectLst->$type, $sizeType); + if (is_numeric($sizeValue)) { + $size[$sizeType] = ChartProperties::xmlToAngle((string) $sizeValue); + } else { + $size[$sizeType] = null; + } + } + $colorArray = $this->readColor($sppr->effectLst->$type); + $chartObject + ->setShadowProperty('effect', $type) + ->setShadowProperty('blur', $blur) + ->setShadowProperty('direction', $direction) + ->setShadowProperty('distance', $dist) + ->setShadowProperty('algn', $algn) + ->setShadowProperty('rotWithShape', $rot) + ->setShadowProperty('size', $size) + ->setShadowProperty('color', $colorArray); + } + } + + private const SHADOW_TYPES = [ + 'outerShdw', + 'innerShdw', + ]; + + /** @return array{type: ?string, value: ?string, alpha: ?int, brightness: ?int} */ + private function readColor(SimpleXMLElement $colorXml): array + { + $result = [ + 'type' => null, + 'value' => null, + 'alpha' => null, + 'brightness' => null, + ]; + foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { + if (isset($colorXml->$type)) { + $result['type'] = $type; + $result['value'] = self::getAttributeString($colorXml->$type, 'val'); + if (isset($colorXml->$type->alpha)) { + $alpha = self::getAttributeString($colorXml->$type->alpha, 'val'); + if (is_numeric($alpha)) { + $result['alpha'] = ChartColor::alphaFromXml($alpha); + } + } + if (isset($colorXml->$type->lumMod)) { + $brightness = self::getAttributeString($colorXml->$type->lumMod, 'val'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } + + break; + } + } + + return $result; + } + + private function readLineStyle(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void + { + if (!isset($chartObject, $chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + + if (!isset($sppr->ln)) { + return; + } + $lineWidth = null; + $lineWidthTemp = self::getAttributeString($sppr->ln, 'w'); + if (is_numeric($lineWidthTemp)) { + $lineWidth = ChartProperties::xmlToPoints($lineWidthTemp); + } + /** @var string $compoundType */ + $compoundType = self::getAttributeString($sppr->ln, 'cmpd'); + /** @var string $dashType */ + $dashType = self::getAttributeString($sppr->ln->prstDash, 'val'); + /** @var string $capType */ + $capType = self::getAttributeString($sppr->ln, 'cap'); + if (isset($sppr->ln->miter)) { + $joinType = ChartProperties::LINE_STYLE_JOIN_MITER; + } elseif (isset($sppr->ln->bevel)) { + $joinType = ChartProperties::LINE_STYLE_JOIN_BEVEL; + } else { + $joinType = ''; + } + $headArrowSize = 0; + $endArrowSize = 0; + $headArrowType = self::getAttributeString($sppr->ln->headEnd, 'type'); + $headArrowWidth = self::getAttributeString($sppr->ln->headEnd, 'w'); + $headArrowLength = self::getAttributeString($sppr->ln->headEnd, 'len'); + $endArrowType = self::getAttributeString($sppr->ln->tailEnd, 'type'); + $endArrowWidth = self::getAttributeString($sppr->ln->tailEnd, 'w'); + $endArrowLength = self::getAttributeString($sppr->ln->tailEnd, 'len'); + $chartObject->setLineStyleProperties( + $lineWidth, + $compoundType, + $dashType, + $capType, + $joinType, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize, + $headArrowWidth, + $headArrowLength, + $endArrowWidth, + $endArrowLength + ); + $colorArray = $this->readColor($sppr->ln->solidFill); + $chartObject->getLineColor()->setColorPropertiesArray($colorArray); + } + + private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void + { + if (!isset($whichAxis)) { + return; + } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttributeString($chartDetail->delete, 'val')); + } + if (isset($chartDetail->numFmt)) { + $whichAxis->setAxisNumberProperties( + (string) self::getAttributeString($chartDetail->numFmt, 'formatCode'), + null, + (int) self::getAttributeInteger($chartDetail->numFmt, 'sourceLinked') + ); + } + if (isset($chartDetail->crossBetween)) { + $whichAxis->setCrossBetween((string) self::getAttributeString($chartDetail->crossBetween, 'val')); + } + if (isset($chartDetail->dispUnits, $chartDetail->dispUnits->builtInUnit)) { + $whichAxis->setAxisOption('dispUnitsBuiltIn', (string) self::getAttributeString($chartDetail->dispUnits->builtInUnit, 'val')); + if (isset($chartDetail->dispUnits->dispUnitsLbl)) { + $whichAxis->setDispUnitsTitle(new Title()); + // TODO parse title elements + } + } + if (isset($chartDetail->majorTickMark)) { + $whichAxis->setAxisOption('major_tick_mark', (string) self::getAttributeString($chartDetail->majorTickMark, 'val')); + } + if (isset($chartDetail->minorTickMark)) { + $whichAxis->setAxisOption('minor_tick_mark', (string) self::getAttributeString($chartDetail->minorTickMark, 'val')); + } + if (isset($chartDetail->tickLblPos)) { + $whichAxis->setAxisOption('axis_labels', (string) self::getAttributeString($chartDetail->tickLblPos, 'val')); + } + if (isset($chartDetail->crosses)) { + $whichAxis->setAxisOption('horizontal_crosses', (string) self::getAttributeString($chartDetail->crosses, 'val')); + } + if (isset($chartDetail->crossesAt)) { + $whichAxis->setAxisOption('horizontal_crosses_value', (string) self::getAttributeString($chartDetail->crossesAt, 'val')); + } + if (isset($chartDetail->scaling->logBase)) { + $whichAxis->setAxisOption('logBase', (string) self::getAttributeString($chartDetail->scaling->logBase, 'val')); + } + if (isset($chartDetail->scaling->orientation)) { + $whichAxis->setAxisOption('orientation', (string) self::getAttributeString($chartDetail->scaling->orientation, 'val')); + } + if (isset($chartDetail->scaling->max)) { + $whichAxis->setAxisOption('maximum', (string) self::getAttributeString($chartDetail->scaling->max, 'val')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttributeString($chartDetail->scaling->min, 'val')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttributeString($chartDetail->scaling->min, 'val')); + } + if (isset($chartDetail->majorUnit)) { + $whichAxis->setAxisOption('major_unit', (string) self::getAttributeString($chartDetail->majorUnit, 'val')); + } + if (isset($chartDetail->minorUnit)) { + $whichAxis->setAxisOption('minor_unit', (string) self::getAttributeString($chartDetail->minorUnit, 'val')); + } + if (isset($chartDetail->baseTimeUnit)) { + $whichAxis->setAxisOption('baseTimeUnit', (string) self::getAttributeString($chartDetail->baseTimeUnit, 'val')); + } + if (isset($chartDetail->majorTimeUnit)) { + $whichAxis->setAxisOption('majorTimeUnit', (string) self::getAttributeString($chartDetail->majorTimeUnit, 'val')); + } + if (isset($chartDetail->minorTimeUnit)) { + $whichAxis->setAxisOption('minorTimeUnit', (string) self::getAttributeString($chartDetail->minorTimeUnit, 'val')); + } + if (isset($chartDetail->txPr)) { + $children = $chartDetail->txPr->children($this->aNamespace); + $addAxisText = false; + $axisText = new AxisText(); + if (isset($children->bodyPr)) { + $textRotation = self::getAttributeString($children->bodyPr, 'rot'); + if (is_numeric($textRotation)) { + $axisText->setRotation((int) ChartProperties::xmlToAngle($textRotation)); + $addAxisText = true; + } + } + if (isset($children->p->pPr->defRPr)) { + $font = $this->parseFont($children->p); + if ($font !== null) { + $axisText->setFont($font); + $addAxisText = true; + } + } + if (isset($children->p->pPr->defRPr->effectLst)) { + $this->readEffects($children->p->pPr->defRPr, $axisText, false); + $addAxisText = true; + } + if ($addAxisText) { + $whichAxis->setAxisText($axisText); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php new file mode 100644 index 0000000..347b1b4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -0,0 +1,244 @@ +worksheet = $workSheet; + $this->worksheetXml = $worksheetXml; + } + + /** + * Set Worksheet column attributes by attributes array passed. + * + * @param string $columnAddress A, B, ... DX, ... + * @param array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float, width?: int} $columnAttributes array of attributes (indexes are attribute name, values are value) + */ + private function setColumnAttributes(string $columnAddress, array $columnAttributes): void + { + if (isset($columnAttributes['xfIndex'])) { + $this->worksheet->getColumnDimension($columnAddress)->setXfIndex($columnAttributes['xfIndex']); + } + if (isset($columnAttributes['visible'])) { + $this->worksheet->getColumnDimension($columnAddress)->setVisible($columnAttributes['visible']); + } + if (isset($columnAttributes['collapsed'])) { + $this->worksheet->getColumnDimension($columnAddress)->setCollapsed($columnAttributes['collapsed']); + } + if (isset($columnAttributes['outlineLevel'])) { + $this->worksheet->getColumnDimension($columnAddress)->setOutlineLevel($columnAttributes['outlineLevel']); + } + if (isset($columnAttributes['width'])) { + $this->worksheet->getColumnDimension($columnAddress)->setWidth($columnAttributes['width']); + } + } + + /** + * Set Worksheet row attributes by attributes array passed. + * + * @param int $rowNumber 1, 2, 3, ... 99, ... + * @param array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float, customFormat?: bool, ht?: float} $rowAttributes array of attributes (indexes are attribute name, values are value) + * 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'rowHeight', ... ? + */ + private function setRowAttributes(int $rowNumber, array $rowAttributes): void + { + if (isset($rowAttributes['xfIndex'])) { + $this->worksheet->getRowDimension($rowNumber) + ->setXfIndex($rowAttributes['xfIndex']); + } + if (isset($rowAttributes['visible'])) { + $this->worksheet->getRowDimension($rowNumber) + ->setVisible($rowAttributes['visible']); + } + if (isset($rowAttributes['collapsed'])) { + $this->worksheet->getRowDimension($rowNumber) + ->setCollapsed($rowAttributes['collapsed']); + } + if (isset($rowAttributes['outlineLevel'])) { + $this->worksheet->getRowDimension($rowNumber) + ->setOutlineLevel($rowAttributes['outlineLevel']); + } + if (isset($rowAttributes['customFormat'], $rowAttributes['rowHeight'])) { + $this->worksheet->getRowDimension($rowNumber) + ->setCustomFormat($rowAttributes['customFormat'], $rowAttributes['rowHeight']); + } elseif (isset($rowAttributes['rowHeight'])) { + $this->worksheet->getRowDimension($rowNumber) + ->setRowHeight($rowAttributes['rowHeight']); + } + } + + public function load(?IReadFilter $readFilter = null, bool $readDataOnly = false, bool $ignoreRowsWithNoCells = false): bool + { + if ($this->worksheetXml === null) { + return false; + } + if ($readFilter !== null && $readFilter::class === DefaultReadFilter::class) { + $readFilter = null; + } + + $columnsAttributes = []; + $rowsAttributes = []; + if (isset($this->worksheetXml->cols)) { + $columnsAttributes = $this->readColumnAttributes($this->worksheetXml->cols, $readDataOnly); + } + + if ($this->worksheetXml->sheetData && $this->worksheetXml->sheetData->row) { + $rowsAttributes = $this->readRowAttributes($this->worksheetXml->sheetData->row, $readDataOnly, $ignoreRowsWithNoCells, $readFilter !== null); + } + + // set columns/rows attributes + $columnsAttributesAreSet = []; + foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) { + if ( + $readFilter === null + || !$this->isFilteredColumn($readFilter, $columnCoordinate, $rowsAttributes) + ) { + if (!isset($columnsAttributesAreSet[$columnCoordinate])) { + /** @var array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float, width?: int} $columnAttributes */ + $this->setColumnAttributes($columnCoordinate, $columnAttributes); + $columnsAttributesAreSet[$columnCoordinate] = true; + } + } + } + + $rowsAttributesAreSet = []; + foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) { + if ( + $readFilter === null + || !$this->isFilteredRow($readFilter, $rowCoordinate, $columnsAttributes) + ) { + if (!isset($rowsAttributesAreSet[$rowCoordinate])) { + /** @var array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float} $rowAttributes */ + $this->setRowAttributes($rowCoordinate, $rowAttributes); + $rowsAttributesAreSet[$rowCoordinate] = true; + } + } + } + + return true; + } + + /** @param mixed[] $rowsAttributes */ + private function isFilteredColumn(IReadFilter $readFilter, string $columnCoordinate, array $rowsAttributes): bool + { + foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) { + if ($readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { + return false; + } + } + + return true; + } + + /** @return mixed[] */ + private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array + { + $columnAttributes = []; + + foreach ($worksheetCols->col as $columnx) { + $column = $columnx->attributes(); + if ($column !== null) { + $startColumn = Coordinate::stringFromColumnIndex((int) $column['min']); + $endColumn = Coordinate::stringFromColumnIndex((int) $column['max']); + StringHelper::stringIncrement($endColumn); + for ($columnAddress = $startColumn; $columnAddress !== $endColumn; StringHelper::stringIncrement($columnAddress)) { + $columnAttributes[$columnAddress] = $this->readColumnRangeAttributes($column, $readDataOnly); + + if ((int) ($column['max']) == 16384) { + break; + } + } + } + } + + return $columnAttributes; + } + + /** @return mixed[] */ + private function readColumnRangeAttributes(?SimpleXMLElement $column, bool $readDataOnly): array + { + $columnAttributes = []; + if ($column !== null) { + if (isset($column['style']) && !$readDataOnly) { + $columnAttributes['xfIndex'] = (int) $column['style']; + } + if (isset($column['hidden']) && self::boolean($column['hidden'])) { + $columnAttributes['visible'] = false; + } + if (isset($column['collapsed']) && self::boolean($column['collapsed'])) { + $columnAttributes['collapsed'] = true; + } + if (isset($column['outlineLevel']) && ((int) $column['outlineLevel']) > 0) { + $columnAttributes['outlineLevel'] = (int) $column['outlineLevel']; + } + if (isset($column['width'])) { + $columnAttributes['width'] = (float) $column['width']; + } + } + + return $columnAttributes; + } + + /** @param mixed[] $columnsAttributes */ + private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool + { + foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) { + if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { + return true; + } + } + + return false; + } + + /** @return mixed[] */ + private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly, bool $ignoreRowsWithNoCells, bool $readFilterIsNotNull): array + { + $rowAttributes = []; + + foreach ($worksheetRow as $rowx) { + $row = $rowx->attributes(); + if ($row !== null && (!$ignoreRowsWithNoCells || isset($rowx->c))) { + $rowIndex = (int) $row['r']; + if (!$readDataOnly) { + if (isset($row['ht'])) { + $rowAttributes[$rowIndex]['rowHeight'] = (float) $row['ht']; + } + if (isset($row['customFormat']) && self::boolean($row['customFormat'])) { + $rowAttributes[$rowIndex]['customFormat'] = true; + } + if (isset($row['hidden']) && self::boolean($row['hidden'])) { + $rowAttributes[$rowIndex]['visible'] = false; + } + if (isset($row['collapsed']) && self::boolean($row['collapsed'])) { + $rowAttributes[$rowIndex]['collapsed'] = true; + } + if (isset($row['outlineLevel']) && (int) $row['outlineLevel'] > 0) { + $rowAttributes[$rowIndex]['outlineLevel'] = (int) $row['outlineLevel']; + } + if (isset($row['s'])) { + $rowAttributes[$rowIndex]['xfIndex'] = (int) $row['s']; + } + } + if ($readFilterIsNotNull && empty($rowAttributes[$rowIndex])) { + $rowAttributes[$rowIndex]['exists'] = true; + } + } + } + + return $rowAttributes; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php new file mode 100644 index 0000000..e2cf3b2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -0,0 +1,414 @@ +worksheet = $workSheet; + $this->worksheetXml = $worksheetXml; + $this->dxfs = $dxfs; + $this->styleReader = $styleReader; + } + + public function load(): void + { + $selectedCells = $this->worksheet->getSelectedCells(); + + $this->setConditionalStyles( + $this->worksheet, + $this->readConditionalStyles($this->worksheetXml), + $this->worksheetXml->extLst + ); + + $this->worksheet->setSelectedCells($selectedCells); + } + + public function loadFromExt(): void + { + $selectedCells = $this->worksheet->getSelectedCells(); + + $this->ns = $this->worksheetXml->getNamespaces(true); + $this->setConditionalsFromExt( + $this->readConditionalsFromExt($this->worksheetXml->extLst) + ); + + $this->worksheet->setSelectedCells($selectedCells); + } + + /** @param Conditional[][] $conditionals */ + private function setConditionalsFromExt(array $conditionals): void + { + foreach ($conditionals as $conditionalRange => $cfRules) { + ksort($cfRules); + // Priority is used as the key for sorting; but may not start at 0, + // so we use array_values to reset the index after sorting. + $existing = $this->worksheet->getConditionalStylesCollection(); + if (array_key_exists($conditionalRange, $existing)) { + $conditionalStyle = $existing[$conditionalRange]; + $cfRules = array_merge($conditionalStyle, $cfRules); + } + $this->worksheet->getStyle($conditionalRange) + ->setConditionalStyles(array_values($cfRules)); + } + } + + /** @return array> */ + private function readConditionalsFromExt(SimpleXMLElement $extLst): array + { + $conditionals = []; + if (!isset($extLst->ext)) { + return $conditionals; + } + + foreach ($extLst->ext as $extlstcond) { + $extAttrs = $extlstcond->attributes() ?? []; + $extUri = (string) ($extAttrs['uri'] ?? ''); + if ($extUri !== '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') { + continue; + } + $conditionalFormattingRuleXml = $extlstcond->children($this->ns['x14']); + if (!$conditionalFormattingRuleXml->conditionalFormattings) { + return []; + } + + foreach ($conditionalFormattingRuleXml->children($this->ns['x14']) as $extFormattingXml) { + $extFormattingRangeXml = $extFormattingXml->children($this->ns['xm']); + if (!$extFormattingRangeXml->sqref) { + continue; + } + + $sqref = (string) $extFormattingRangeXml->sqref; + $extCfRuleXml = $extFormattingXml->cfRule; + + $attributes = $extCfRuleXml->attributes(); + if (!$attributes) { + continue; + } + $conditionType = (string) $attributes->type; + if ( + !Conditional::isValidConditionType($conditionType) + || $conditionType === Conditional::CONDITION_DATABAR + ) { + continue; + } + + $priority = (int) $attributes->priority; + + $conditional = $this->readConditionalRuleFromExt($extCfRuleXml, $attributes); + $cfStyle = $this->readStyleFromExt($extCfRuleXml); + $conditional->setStyle($cfStyle); + $conditionals[$sqref][$priority] = $conditional; + } + } + + return $conditionals; + } + + private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleXMLElement $attributes): Conditional + { + $conditionType = (string) $attributes->type; + $operatorType = (string) $attributes->operator; + $priority = (int) (string) $attributes->priority; + $stopIfTrue = (int) (string) $attributes->stopIfTrue; + + $operands = []; + foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) { + $operands[] = (string) $cfRuleOperandsXml; + } + + $conditional = new Conditional(); + $conditional->setConditionType($conditionType); + $conditional->setOperatorType($operatorType); + $conditional->setPriority($priority); + $conditional->setStopIfTrue($stopIfTrue === 1); + if ( + $conditionType === Conditional::CONDITION_CONTAINSTEXT + || $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT + || $conditionType === Conditional::CONDITION_BEGINSWITH + || $conditionType === Conditional::CONDITION_ENDSWITH + || $conditionType === Conditional::CONDITION_TIMEPERIOD + ) { + $conditional->setText(array_pop($operands) ?? ''); + } + $conditional->setConditions($operands); + + return $conditional; + } + + private function readStyleFromExt(SimpleXMLElement $extCfRuleXml): Style + { + $cfStyle = new Style(false, true); + if ($extCfRuleXml->dxf) { + $styleXML = $extCfRuleXml->dxf->children(); + + if ($styleXML->borders) { + $this->styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders); + } + if ($styleXML->fill) { + $this->styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill); + } + if ($styleXML->font) { + $this->styleReader->readFontStyle($cfStyle->getFont(), $styleXML->font); + } + } + + return $cfStyle; + } + + /** @return mixed[] */ + private function readConditionalStyles(SimpleXMLElement $xmlSheet): array + { + $conditionals = []; + foreach ($xmlSheet->conditionalFormatting as $conditional) { + foreach ($conditional->cfRule as $cfRule) { + if (Conditional::isValidConditionType((string) $cfRule['type']) && (!isset($cfRule['dxfId']) || isset($this->dxfs[(int) ($cfRule['dxfId'])]))) { + $conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule; + } elseif ((string) $cfRule['type'] == Conditional::CONDITION_DATABAR) { + $conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule; + } + } + } + + return $conditionals; + } + + /** @param mixed[] $conditionals */ + private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void + { + foreach ($conditionals as $cellRangeReference => $cfRules) { + /** @var mixed[] $cfRules */ + ksort($cfRules); // no longer needed for Xlsx, but helps Xls + $conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst); + + // Extract all cell references in $cellRangeReference + // N.B. In Excel UI, intersection is space and union is comma. + // But in Xml, intersection is comma and union is space. + $cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference)); + + foreach ($conditionalStyles as $cs) { + $scale = $cs->getColorScale(); + if ($scale !== null) { + $scale->setSqRef($cellRangeReference, $worksheet); + } + } + $worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles); + } + } + + /** + * @param mixed[] $cfRules + * + * @return Conditional[] + */ + private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array + { + /** @var ConditionalFormattingRuleExtension[] */ + $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); + $conditionalStyles = []; + + /** @var SimpleXMLElement $cfRule */ + foreach ($cfRules as $cfRule) { + $objConditional = new Conditional(); + $objConditional->setConditionType((string) $cfRule['type']); + $objConditional->setOperatorType((string) $cfRule['operator']); + $objConditional->setPriority((int) (string) $cfRule['priority']); + $objConditional->setNoFormatSet(!isset($cfRule['dxfId'])); + + if ((string) $cfRule['text'] != '') { + $objConditional->setText((string) $cfRule['text']); + } elseif ((string) $cfRule['timePeriod'] != '') { + $objConditional->setText((string) $cfRule['timePeriod']); + } + + if (isset($cfRule['stopIfTrue']) && (int) $cfRule['stopIfTrue'] === 1) { + $objConditional->setStopIfTrue(true); + } + + if (count($cfRule->formula) >= 1) { + foreach ($cfRule->formula as $formulax) { + $formula = (string) $formulax; + $formula = str_replace(['_xlfn.', '_xlws.'], '', $formula); + if ($formula === 'TRUE') { + $objConditional->addCondition(true); + } elseif ($formula === 'FALSE') { + $objConditional->addCondition(false); + } else { + $objConditional->addCondition($formula); + } + } + } else { + $objConditional->addCondition(''); + } + + if (isset($cfRule->dataBar)) { + $objConditional->setDataBar( + $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) + ); + } elseif (isset($cfRule->colorScale)) { + $objConditional->setColorScale( + $this->readColorScale($cfRule) + ); + } elseif (isset($cfRule->iconSet)) { + $objConditional->setIconSet($this->readIconSet($cfRule)); + } elseif (isset($cfRule['dxfId'])) { + $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); + } + + $conditionalStyles[] = $objConditional; + } + + return $conditionalStyles; + } + + /** @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtensions */ + private function readDataBarOfConditionalRule(SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar + { + $dataBar = new ConditionalDataBar(); + //dataBar attribute + if (isset($cfRule->dataBar['showValue'])) { + $dataBar->setShowValue((bool) $cfRule->dataBar['showValue']); + } + + //dataBar children + //conditionalFormatValueObjects + $cfvoXml = $cfRule->dataBar->cfvo; + $cfvoIndex = 0; + foreach ((count($cfvoXml) > 1 ? $cfvoXml : [$cfvoXml]) as $cfvo) { //* @phpstan-ignore-line + /** @var SimpleXMLElement $cfvo */ + if ($cfvoIndex === 0) { + $dataBar->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val'])); + } + if ($cfvoIndex === 1) { + $dataBar->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val'])); + } + ++$cfvoIndex; + } + + //color + if (isset($cfRule->dataBar->color)) { + $dataBar->setColor($this->styleReader->readColor($cfRule->dataBar->color)); + } + //extLst + $this->readDataBarExtLstOfConditionalRule($dataBar, $cfRule, $conditionalFormattingRuleExtensions); + + return $dataBar; + } + + private function readColorScale(SimpleXMLElement|stdClass $cfRule): ConditionalColorScale + { + $colorScale = new ConditionalColorScale(); + /** @var SimpleXMLElement $cfRule */ + $count = count($cfRule->colorScale->cfvo); + $idx = 0; + foreach ($cfRule->colorScale->cfvo as $cfvoXml) { + $attr = $cfvoXml->attributes() ?? []; + $type = (string) ($attr['type'] ?? ''); + $val = $attr['val'] ?? null; + if ($idx === 0) { + $method = 'setMinimumConditionalFormatValueObject'; + } elseif ($idx === 1 && $count === 3) { + $method = 'setMidpointConditionalFormatValueObject'; + } else { + $method = 'setMaximumConditionalFormatValueObject'; + } + if ($type !== 'formula') { + $colorScale->$method(new ConditionalFormatValueObject($type, $val)); + } else { + $colorScale->$method(new ConditionalFormatValueObject($type, null, $val)); + } + ++$idx; + } + $idx = 0; + foreach ($cfRule->colorScale->color as $color) { + $rgb = $this->styleReader->readColor($color); + if ($idx === 0) { + $colorScale->setMinimumColor(new Color($rgb)); + } elseif ($idx === 1 && $count === 3) { + $colorScale->setMidpointColor(new Color($rgb)); + } else { + $colorScale->setMaximumColor(new Color($rgb)); + } + ++$idx; + } + + return $colorScale; + } + + private function readIconSet(SimpleXMLElement $cfRule): ConditionalIconSet + { + $iconSet = new ConditionalIconSet(); + + if (isset($cfRule->iconSet['iconSet'])) { + $iconSet->setIconSetType(IconSetValues::from($cfRule->iconSet['iconSet'])); + } + if (isset($cfRule->iconSet['reverse'])) { + $iconSet->setReverse('1' === (string) $cfRule->iconSet['reverse']); + } + if (isset($cfRule->iconSet['showValue'])) { + $iconSet->setShowValue('1' === (string) $cfRule->iconSet['showValue']); + } + if (isset($cfRule->iconSet['custom'])) { + $iconSet->setCustom('1' === (string) $cfRule->iconSet['custom']); + } + + $cfvos = []; + foreach ($cfRule->iconSet->cfvo as $cfvoXml) { + $type = (string) $cfvoXml['type']; + $value = (string) ($cfvoXml['val'] ?? ''); + $cfvo = new ConditionalFormatValueObject($type, $value); + if (isset($cfvoXml['gte'])) { + $cfvo->setGreaterThanOrEqual('1' === (string) $cfvoXml['gte']); + } + $cfvos[] = $cfvo; + } + $iconSet->setCfvos($cfvos); + + // TODO: The cfIcon element is not implemented yet. + + return $iconSet; + } + + /** @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtensions */ + private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): void + { + if (isset($cfRule->extLst)) { + $ns = $cfRule->extLst->getNamespaces(true); + foreach ((count($cfRule->extLst) > 0 ? $cfRule->extLst->ext : [$cfRule->extLst->ext]) as $ext) { //* @phpstan-ignore-line + /** @var SimpleXMLElement $ext */ + $extId = (string) $ext->children($ns['x14'])->id; + if (isset($conditionalFormattingRuleExtensions[$extId]) && (string) $ext['uri'] === '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}') { + $dataBar->setConditionalFormattingRuleExt($conditionalFormattingRuleExtensions[$extId]); + } + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php new file mode 100644 index 0000000..bab9099 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -0,0 +1,58 @@ +worksheet = $workSheet; + $this->worksheetXml = $worksheetXml; + } + + public function load(): void + { + foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { + // Uppercase coordinate + $range = strtoupper((string) $dataValidation['sqref']); + $rangeSet = explode(' ', $range); + foreach ($rangeSet as $range) { + if (preg_match('/^[A-Z]{1,3}\d{1,7}/', $range, $matches) === 1) { + // Ensure left/top row of range exists, thereby + // adjusting high row/column. + $this->worksheet->getCell($matches[0]); + } + } + } + foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { + // Uppercase coordinate + $range = strtoupper((string) $dataValidation['sqref']); + $docValidation = new DataValidation(); + $docValidation->setType((string) $dataValidation['type']); + $docValidation->setErrorStyle((string) $dataValidation['errorStyle']); + $docValidation->setOperator((string) $dataValidation['operator']); + $docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN)); + // showDropDown is inverted (works as hideDropDown if true) + $docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN)); + $docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN)); + $docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN)); + $docValidation->setErrorTitle((string) $dataValidation['errorTitle']); + $docValidation->setError((string) $dataValidation['error']); + $docValidation->setPromptTitle((string) $dataValidation['promptTitle']); + $docValidation->setPrompt((string) $dataValidation['prompt']); + $docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1)); + $docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2)); + $this->worksheet->setDataValidation($range, $docValidation); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php new file mode 100644 index 0000000..2c2d4af --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -0,0 +1,63 @@ +worksheet = $workSheet; + } + + public function readHyperlinks(SimpleXMLElement $relsWorksheet): void + { + foreach ($relsWorksheet->children(Namespaces::RELATIONSHIPS)->Relationship as $elementx) { + $element = Xlsx::getAttributes($elementx); + if ($element->Type == Namespaces::HYPERLINK) { + $this->hyperlinks[(string) $element->Id] = (string) $element->Target; + } + } + } + + public function setHyperlinks(SimpleXMLElement $worksheetXml): void + { + foreach ($worksheetXml->children(Namespaces::MAIN)->hyperlink as $hyperlink) { + $this->setHyperlink($hyperlink, $this->worksheet); + } + } + + private function setHyperlink(SimpleXMLElement $hyperlink, Worksheet $worksheet): void + { + // Link url + $linkRel = Xlsx::getAttributes($hyperlink, Namespaces::SCHEMA_OFFICE_DOCUMENT); + + $attributes = Xlsx::getAttributes($hyperlink); + foreach (Coordinate::extractAllCellReferencesInRange($attributes->ref) as $cellReference) { + $cell = $worksheet->getCell($cellReference); + if (isset($linkRel['id'])) { + $hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']] ?? ''; + if (isset($attributes['location'])) { + $hyperlinkUrl .= '#' . (string) $attributes['location']; + } + $cell->getHyperlink()->setUrl($hyperlinkUrl); + } elseif (isset($attributes['location'])) { + $cell->getHyperlink()->setUrl('sheet://' . (string) $attributes['location']); + } + + // Tooltip + if (isset($attributes['tooltip'])) { + $cell->getHyperlink()->setTooltip((string) $attributes['tooltip']); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php new file mode 100644 index 0000000..7356cd8 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -0,0 +1,124 @@ +worksheet = $workSheet; + $this->worksheetXml = $worksheetXml; + } + + /** + * @param mixed[] $unparsedLoadedData + * + * @return mixed[] + */ + public function load(array $unparsedLoadedData): array + { + $worksheetXml = $this->worksheetXml; + if ($worksheetXml === null) { + return $unparsedLoadedData; + } + + $this->margins($worksheetXml, $this->worksheet); + $unparsedLoadedData = $this->pageSetup($worksheetXml, $this->worksheet, $unparsedLoadedData); + $this->headerFooter($worksheetXml, $this->worksheet); + $this->pageBreaks($worksheetXml, $this->worksheet); + + return $unparsedLoadedData; + } + + private function margins(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + if ($xmlSheet->pageMargins) { + $docPageMargins = $worksheet->getPageMargins(); + $docPageMargins->setLeft((float) ($xmlSheet->pageMargins['left'])); + $docPageMargins->setRight((float) ($xmlSheet->pageMargins['right'])); + $docPageMargins->setTop((float) ($xmlSheet->pageMargins['top'])); + $docPageMargins->setBottom((float) ($xmlSheet->pageMargins['bottom'])); + $docPageMargins->setHeader((float) ($xmlSheet->pageMargins['header'])); + $docPageMargins->setFooter((float) ($xmlSheet->pageMargins['footer'])); + } + } + + /** + * @param mixed[] $unparsedLoadedData + * + * @return mixed[] + */ + private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array + { + if ($xmlSheet->pageSetup) { + $docPageSetup = $worksheet->getPageSetup(); + + if (isset($xmlSheet->pageSetup['orientation'])) { + $docPageSetup->setOrientation((string) $xmlSheet->pageSetup['orientation']); + } + if (isset($xmlSheet->pageSetup['paperSize'])) { + $docPageSetup->setPaperSize((int) ($xmlSheet->pageSetup['paperSize'])); + } + if (isset($xmlSheet->pageSetup['scale'])) { + $docPageSetup->setScale((int) ($xmlSheet->pageSetup['scale']), false); + } + if (isset($xmlSheet->pageSetup['fitToHeight']) && (int) ($xmlSheet->pageSetup['fitToHeight']) >= 0) { + $docPageSetup->setFitToHeight((int) ($xmlSheet->pageSetup['fitToHeight']), false); + } + if (isset($xmlSheet->pageSetup['fitToWidth']) && (int) ($xmlSheet->pageSetup['fitToWidth']) >= 0) { + $docPageSetup->setFitToWidth((int) ($xmlSheet->pageSetup['fitToWidth']), false); + } + if ( + isset($xmlSheet->pageSetup['firstPageNumber'], $xmlSheet->pageSetup['useFirstPageNumber']) + && self::boolean((string) $xmlSheet->pageSetup['useFirstPageNumber']) + ) { + $docPageSetup->setFirstPageNumber((int) ($xmlSheet->pageSetup['firstPageNumber'])); + } + if (isset($xmlSheet->pageSetup['pageOrder'])) { + $docPageSetup->setPageOrder((string) $xmlSheet->pageSetup['pageOrder']); + } + + $relAttributes = $xmlSheet->pageSetup->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT); + if (isset($relAttributes['id'])) { + $relid = (string) $relAttributes['id']; + if (!str_ends_with($relid, 'ps')) { + $relid .= 'ps'; + } + /** @var mixed[][][] $unparsedLoadedData */ + $unparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'] = $relid; + } + } + + return $unparsedLoadedData; + } + + private function headerFooter(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + if ($xmlSheet->headerFooter) { + $docHeaderFooter = $worksheet->getHeaderFooter(); + + if ( + isset($xmlSheet->headerFooter['differentOddEven']) + && self::boolean((string) $xmlSheet->headerFooter['differentOddEven']) + ) { + $docHeaderFooter->setDifferentOddEven(true); + } else { + $docHeaderFooter->setDifferentOddEven(false); + } + if ( + isset($xmlSheet->headerFooter['differentFirst']) + && self::boolean((string) $xmlSheet->headerFooter['differentFirst']) + ) { + $docHeaderFooter->setDifferentFirst(true); + } else { + $docHeaderFooter->setDifferentFirst(false); + } + if ( + isset($xmlSheet->headerFooter['scaleWithDoc']) + && !self::boolean((string) $xmlSheet->headerFooter['scaleWithDoc']) + ) { + $docHeaderFooter->setScaleWithDocument(false); + } else { + $docHeaderFooter->setScaleWithDocument(true); + } + if ( + isset($xmlSheet->headerFooter['alignWithMargins']) + && !self::boolean((string) $xmlSheet->headerFooter['alignWithMargins']) + ) { + $docHeaderFooter->setAlignWithMargins(false); + } else { + $docHeaderFooter->setAlignWithMargins(true); + } + + $docHeaderFooter->setOddHeader((string) $xmlSheet->headerFooter->oddHeader); + $docHeaderFooter->setOddFooter((string) $xmlSheet->headerFooter->oddFooter); + $docHeaderFooter->setEvenHeader((string) $xmlSheet->headerFooter->evenHeader); + $docHeaderFooter->setEvenFooter((string) $xmlSheet->headerFooter->evenFooter); + $docHeaderFooter->setFirstHeader((string) $xmlSheet->headerFooter->firstHeader); + $docHeaderFooter->setFirstFooter((string) $xmlSheet->headerFooter->firstFooter); + } + } + + private function pageBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + if ($xmlSheet->rowBreaks && $xmlSheet->rowBreaks->brk) { + $this->rowBreaks($xmlSheet, $worksheet); + } + if ($xmlSheet->colBreaks && $xmlSheet->colBreaks->brk) { + $this->columnBreaks($xmlSheet, $worksheet); + } + } + + private function rowBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + foreach ($xmlSheet->rowBreaks->brk as $brk) { + $rowBreakMax = /*isset($brk['max']) ? ((int) $brk['max']) :*/ -1; + if ($brk['man']) { + $worksheet->setBreak("A{$brk['id']}", Worksheet::BREAK_ROW, $rowBreakMax); + } + } + } + + private function columnBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void + { + foreach ($xmlSheet->colBreaks->brk as $brk) { + if ($brk['man']) { + $worksheet->setBreak( + Coordinate::stringFromColumnIndex(((int) $brk['id']) + 1) . '1', + Worksheet::BREAK_COLUMN + ); + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Properties.php new file mode 100644 index 0000000..a792235 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -0,0 +1,98 @@ +securityScanner = $securityScanner; + $this->docProps = $docProps; + } + + private function extractPropertyData(string $propertyData): ?SimpleXMLElement + { + // okay to omit namespace because everything will be processed by xpath + $obj = simplexml_load_string( + $this->securityScanner->scan($propertyData) + ); + + return $obj === false ? null : $obj; + } + + public function readCoreProperties(string $propertyData): void + { + $xmlCore = $this->extractPropertyData($propertyData); + + if (is_object($xmlCore)) { + $xmlCore->registerXPathNamespace('dc', Namespaces::DC_ELEMENTS); + $xmlCore->registerXPathNamespace('dcterms', Namespaces::DC_TERMS); + $xmlCore->registerXPathNamespace('cp', Namespaces::CORE_PROPERTIES2); + + $this->docProps->setCreator($this->getArrayItem($xmlCore->xpath('dc:creator'))); + $this->docProps->setLastModifiedBy($this->getArrayItem($xmlCore->xpath('cp:lastModifiedBy'))); + $this->docProps->setCreated($this->getArrayItem($xmlCore->xpath('dcterms:created'))); //! respect xsi:type + $this->docProps->setModified($this->getArrayItem($xmlCore->xpath('dcterms:modified'))); //! respect xsi:type + $this->docProps->setTitle($this->getArrayItem($xmlCore->xpath('dc:title'))); + $this->docProps->setDescription($this->getArrayItem($xmlCore->xpath('dc:description'))); + $this->docProps->setSubject($this->getArrayItem($xmlCore->xpath('dc:subject'))); + $this->docProps->setKeywords($this->getArrayItem($xmlCore->xpath('cp:keywords'))); + $this->docProps->setCategory($this->getArrayItem($xmlCore->xpath('cp:category'))); + } + } + + public function readExtendedProperties(string $propertyData): void + { + $xmlCore = $this->extractPropertyData($propertyData); + + if (is_object($xmlCore)) { + if (isset($xmlCore->Company)) { + $this->docProps->setCompany((string) $xmlCore->Company); + } + if (isset($xmlCore->Manager)) { + $this->docProps->setManager((string) $xmlCore->Manager); + } + if (isset($xmlCore->HyperlinkBase)) { + $this->docProps->setHyperlinkBase((string) $xmlCore->HyperlinkBase); + } + } + } + + public function readCustomProperties(string $propertyData): void + { + $xmlCore = $this->extractPropertyData($propertyData); + + if (is_object($xmlCore)) { + foreach ($xmlCore as $xmlProperty) { + /** @var SimpleXMLElement $xmlProperty */ + $cellDataOfficeAttributes = $xmlProperty->attributes(); + if (isset($cellDataOfficeAttributes['name'])) { + $propertyName = (string) $cellDataOfficeAttributes['name']; + $cellDataOfficeChildren = $xmlProperty->children('http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + + $attributeType = $cellDataOfficeChildren->getName(); + /** @var SimpleXMLElement */ + $attributeValue = $cellDataOfficeChildren->{$attributeType}; + $attributeValue = (string) $attributeValue; + $attributeValue = DocumentProperties::convertProperty($attributeValue, $attributeType); + $attributeType = DocumentProperties::convertPropertyType($attributeType); + $this->docProps->setCustomProperty($propertyName, $attributeValue, $attributeType); + } + } + } + } + + /** @param null|false|scalar[] $array */ + private function getArrayItem(null|array|false $array): string + { + return is_array($array) ? (string) ($array[0] ?? '') : ''; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SharedFormula.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SharedFormula.php new file mode 100644 index 0000000..fb7a393 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SharedFormula.php @@ -0,0 +1,26 @@ +master = $master; + $this->formula = $formula; + } + + public function master(): string + { + return $this->master; + } + + public function formula(): string + { + return $this->formula; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php new file mode 100644 index 0000000..9d71443 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -0,0 +1,139 @@ +worksheet = $workSheet; + $this->worksheetXml = $worksheetXml; + } + + public function load(bool $readDataOnly, Styles $styleReader): void + { + if ($this->worksheetXml === null) { + return; + } + + if (isset($this->worksheetXml->sheetPr)) { + $sheetPr = $this->worksheetXml->sheetPr; + $this->tabColor($sheetPr, $styleReader); + $this->codeName($sheetPr); + $this->outlines($sheetPr); + $this->pageSetup($sheetPr); + } + + if (isset($this->worksheetXml->sheetFormatPr)) { + $this->sheetFormat($this->worksheetXml->sheetFormatPr); + } + + if (!$readDataOnly && isset($this->worksheetXml->printOptions)) { + $this->printOptions($this->worksheetXml->printOptions); + } + } + + private function tabColor(SimpleXMLElement $sheetPr, Styles $styleReader): void + { + if (isset($sheetPr->tabColor)) { + $this->worksheet->getTabColor()->setARGB($styleReader->readColor($sheetPr->tabColor)); + } + } + + private function codeName(SimpleXMLElement $sheetPrx): void + { + $sheetPr = $sheetPrx->attributes() ?? []; + if (isset($sheetPr['codeName'])) { + $this->worksheet->setCodeName((string) $sheetPr['codeName'], false); + } + } + + private function outlines(SimpleXMLElement $sheetPr): void + { + if (isset($sheetPr->outlinePr)) { + $attr = $sheetPr->outlinePr->attributes() ?? []; + if ( + isset($attr['summaryRight']) + && !self::boolean((string) $attr['summaryRight']) + ) { + $this->worksheet->setShowSummaryRight(false); + } else { + $this->worksheet->setShowSummaryRight(true); + } + + if ( + isset($attr['summaryBelow']) + && !self::boolean((string) $attr['summaryBelow']) + ) { + $this->worksheet->setShowSummaryBelow(false); + } else { + $this->worksheet->setShowSummaryBelow(true); + } + } + } + + private function pageSetup(SimpleXMLElement $sheetPr): void + { + if (isset($sheetPr->pageSetUpPr)) { + $attr = $sheetPr->pageSetUpPr->attributes() ?? []; + if ( + isset($attr['fitToPage']) + && !self::boolean((string) $attr['fitToPage']) + ) { + $this->worksheet->getPageSetup()->setFitToPage(false); + } else { + $this->worksheet->getPageSetup()->setFitToPage(true); + } + } + } + + private function sheetFormat(SimpleXMLElement $sheetFormatPrx): void + { + $sheetFormatPr = $sheetFormatPrx->attributes() ?? []; + if ( + isset($sheetFormatPr['customHeight']) + && self::boolean((string) $sheetFormatPr['customHeight']) + && isset($sheetFormatPr['defaultRowHeight']) + ) { + $this->worksheet->getDefaultRowDimension() + ->setRowHeight((float) $sheetFormatPr['defaultRowHeight']); + } + + if (isset($sheetFormatPr['defaultColWidth'])) { + $this->worksheet->getDefaultColumnDimension() + ->setWidth((float) $sheetFormatPr['defaultColWidth']); + } + + if ( + isset($sheetFormatPr['zeroHeight']) + && ((string) $sheetFormatPr['zeroHeight'] === '1') + ) { + $this->worksheet->getDefaultRowDimension()->setZeroHeight(true); + } + } + + private function printOptions(SimpleXMLElement $printOptionsx): void + { + $printOptions = $printOptionsx->attributes() ?? []; + // Spec is weird. gridLines (default false) + // and gridLinesSet (default true) must both be true. + if (isset($printOptions['gridLines']) && self::boolean((string) $printOptions['gridLines'])) { + if (!isset($printOptions['gridLinesSet']) || self::boolean((string) $printOptions['gridLinesSet'])) { + $this->worksheet->setPrintGridlines(true); + } + } + if (isset($printOptions['horizontalCentered']) && self::boolean((string) $printOptions['horizontalCentered'])) { + $this->worksheet->getPageSetup()->setHorizontalCentered(true); + } + if (isset($printOptions['verticalCentered']) && self::boolean((string) $printOptions['verticalCentered'])) { + $this->worksheet->getPageSetup()->setVerticalCentered(true); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php new file mode 100644 index 0000000..7c4b8b2 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php @@ -0,0 +1,199 @@ +sheetViewXml = $sheetViewXml; + $this->sheetViewAttributes = Xlsx::testSimpleXml($sheetViewXml->attributes()); + $this->worksheet = $workSheet; + } + + public function load(): void + { + $this->topLeft(); + $this->zoomScale(); + $this->view(); + $this->gridLines(); + $this->headers(); + $this->direction(); + $this->showZeros(); + + $usesPanes = false; + if (isset($this->sheetViewXml->pane)) { + $this->pane(); + $usesPanes = true; + } + if (isset($this->sheetViewXml->selection)) { + foreach ($this->sheetViewXml->selection as $selection) { + $this->selection($selection, $usesPanes); + } + } + } + + private function zoomScale(): void + { + if (isset($this->sheetViewAttributes->zoomScale)) { + $zoomScale = (int) ($this->sheetViewAttributes->zoomScale); + if ($zoomScale <= 0) { + // setZoomScale will throw an Exception if the scale is less than or equals 0 + // that is OK when manually creating documents, but we should be able to read all documents + $zoomScale = 100; + } + + $this->worksheet->getSheetView()->setZoomScale($zoomScale); + } + + if (isset($this->sheetViewAttributes->zoomScaleNormal)) { + $zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScaleNormal); + if ($zoomScaleNormal <= 0) { + // setZoomScaleNormal will throw an Exception if the scale is less than or equals 0 + // that is OK when manually creating documents, but we should be able to read all documents + $zoomScaleNormal = 100; + } + + $this->worksheet->getSheetView()->setZoomScaleNormal($zoomScaleNormal); + } + + if (isset($this->sheetViewAttributes->zoomScalePageLayoutView)) { + $zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScalePageLayoutView); + if ($zoomScaleNormal > 0) { + $this->worksheet->getSheetView()->setZoomScalePageLayoutView($zoomScaleNormal); + } + } + + if (isset($this->sheetViewAttributes->zoomScaleSheetLayoutView)) { + $zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScaleSheetLayoutView); + if ($zoomScaleNormal > 0) { + $this->worksheet->getSheetView()->setZoomScaleSheetLayoutView($zoomScaleNormal); + } + } + } + + private function view(): void + { + if (isset($this->sheetViewAttributes->view)) { + $this->worksheet->getSheetView()->setView((string) $this->sheetViewAttributes->view); + } + } + + private function topLeft(): void + { + if (isset($this->sheetViewAttributes->topLeftCell)) { + $this->worksheet->setTopLeftCell($this->sheetViewAttributes->topLeftCell); + } + } + + private function gridLines(): void + { + if (isset($this->sheetViewAttributes->showGridLines)) { + $this->worksheet->setShowGridLines( + self::boolean((string) $this->sheetViewAttributes->showGridLines) + ); + } + } + + private function headers(): void + { + if (isset($this->sheetViewAttributes->showRowColHeaders)) { + $this->worksheet->setShowRowColHeaders( + self::boolean((string) $this->sheetViewAttributes->showRowColHeaders) + ); + } + } + + private function direction(): void + { + if (isset($this->sheetViewAttributes->rightToLeft)) { + $this->worksheet->setRightToLeft( + self::boolean((string) $this->sheetViewAttributes->rightToLeft) + ); + } + } + + private function showZeros(): void + { + if (isset($this->sheetViewAttributes->showZeros)) { + $this->worksheet->getSheetView()->setShowZeros( + self::boolean((string) $this->sheetViewAttributes->showZeros) + ); + } + } + + private function pane(): void + { + $xSplit = 0; + $ySplit = 0; + $topLeftCell = null; + $paneAttributes = $this->sheetViewXml->pane->attributes(); + + if (isset($paneAttributes->xSplit)) { + $xSplit = (int) ($paneAttributes->xSplit); + $this->worksheet->setXSplit($xSplit); + } + + if (isset($paneAttributes->ySplit)) { + $ySplit = (int) ($paneAttributes->ySplit); + $this->worksheet->setYSplit($ySplit); + } + $paneState = isset($paneAttributes->state) ? ((string) $paneAttributes->state) : ''; + $this->worksheet->setPaneState($paneState); + if (isset($paneAttributes->topLeftCell)) { + $topLeftCell = (string) $paneAttributes->topLeftCell; + $this->worksheet->setPaneTopLeftCell($topLeftCell); + if ($paneState === Worksheet::PANE_FROZEN) { + $this->worksheet->setTopLeftCell($topLeftCell); + } + } + $activePane = isset($paneAttributes->activePane) ? ((string) $paneAttributes->activePane) : 'topLeft'; + $this->worksheet->setActivePane($activePane); + $this->activePane = $activePane; + if ($paneState === Worksheet::PANE_FROZEN || $paneState === Worksheet::PANE_FROZENSPLIT) { + $this->worksheet->freezePane( + Coordinate::stringFromColumnIndex($xSplit + 1) . ($ySplit + 1), + $topLeftCell, + $paneState === Worksheet::PANE_FROZENSPLIT + ); + } + } + + private function selection(?SimpleXMLElement $selection, bool $usesPanes): void + { + $attributes = ($selection === null) ? null : $selection->attributes(); + if ($attributes !== null) { + $position = (string) $attributes->pane; + if ($usesPanes && $position === '') { + $position = 'topLeft'; + } + $activeCell = (string) $attributes->activeCell; + $sqref = (string) $attributes->sqref; + $sqref = explode(' ', $sqref); + $sqref = $sqref[0]; + if ($position === '') { + $this->worksheet->setSelectedCells($sqref); + } else { + $pane = new Pane($position, $sqref, $activeCell); + $this->worksheet->setPane($position, $pane); + if ($position === $this->activePane && $sqref !== '') { + $this->worksheet->setSelectedCells($sqref); + } + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Styles.php new file mode 100644 index 0000000..54e26e7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -0,0 +1,558 @@ + */ + private array $cellStyles = []; + + private SimpleXMLElement $styleXml; + + private string $namespace = ''; + + /** @var array */ + private array $fontCharsets = []; + + /** @return array */ + public function getFontCharsets(): array + { + return $this->fontCharsets; + } + + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + + /** @param string[] $palette */ + public function setWorkbookPalette(array $palette): void + { + $this->workbookPalette = $palette; + } + + private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement + { + $attr = $value->attributes(''); + if ($attr === null || count($attr) === 0) { + $attr = $value->attributes($this->namespace); + } + + return Xlsx::testSimpleXml($attr); + } + + public function setStyleXml(SimpleXMLElement $styleXml): void + { + $this->styleXml = $styleXml; + } + + public function setTheme(Theme $theme): void + { + $this->theme = $theme; + } + + /** + * @param mixed[] $styles + * @param array $cellStyles + */ + public function setStyleBaseData(?Theme $theme = null, array $styles = [], array $cellStyles = []): void + { + $this->theme = $theme; + $this->styles = $styles; + $this->cellStyles = $cellStyles; + } + + public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void + { + if (isset($fontStyleXml->name)) { + $attr = $this->getStyleAttributes($fontStyleXml->name); + if (isset($attr['val'])) { + $fontStyle->setName((string) $attr['val']); + } + if (isset($fontStyleXml->charset)) { + $charsetAttr = $this->getStyleAttributes($fontStyleXml->charset); + if (isset($charsetAttr['val'])) { + $charsetVal = (int) $charsetAttr['val']; + $this->fontCharsets[$fontStyle->getName()] = $charsetVal; + } + } + } + if (isset($fontStyleXml->sz)) { + $attr = $this->getStyleAttributes($fontStyleXml->sz); + if (isset($attr['val'])) { + $fontStyle->setSize((float) $attr['val']); + } + } + if (isset($fontStyleXml->b)) { + $attr = $this->getStyleAttributes($fontStyleXml->b); + $fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val'])); + } + if (isset($fontStyleXml->i)) { + $attr = $this->getStyleAttributes($fontStyleXml->i); + $fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val'])); + } + if (isset($fontStyleXml->strike)) { + $attr = $this->getStyleAttributes($fontStyleXml->strike); + $fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val'])); + } + $fontStyle->getColor() + ->setARGB( + $this->readColor($fontStyleXml->color) + ); + $theme = $this->readColorTheme($fontStyleXml->color); + if ($theme >= 0) { + $fontStyle->getColor()->setTheme($theme); + } + + if (isset($fontStyleXml->u)) { + $attr = $this->getStyleAttributes($fontStyleXml->u); + if (!isset($attr['val'])) { + $fontStyle->setUnderline(Font::UNDERLINE_SINGLE); + } else { + $fontStyle->setUnderline((string) $attr['val']); + } + } + if (isset($fontStyleXml->vertAlign)) { + $attr = $this->getStyleAttributes($fontStyleXml->vertAlign); + if (isset($attr['val'])) { + $verticalAlign = strtolower((string) $attr['val']); + if ($verticalAlign === 'superscript') { + $fontStyle->setSuperscript(true); + } elseif ($verticalAlign === 'subscript') { + $fontStyle->setSubscript(true); + } + } + } + if (isset($fontStyleXml->scheme)) { + $attr = $this->getStyleAttributes($fontStyleXml->scheme); + $fontStyle->setScheme((string) $attr['val']); + } + if (isset($fontStyleXml->auto)) { + $attr = $this->getStyleAttributes($fontStyleXml->auto); + if (isset($attr['val'])) { + $fontStyle->setAutoColor(self::boolean((string) $attr['val'])); + } + } + } + + private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void + { + if ((string) $numfmtStyleXml['formatCode'] !== '') { + $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode'])); + + return; + } + $numfmt = $this->getStyleAttributes($numfmtStyleXml); + if (isset($numfmt['formatCode'])) { + $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode'])); + } + } + + public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml): void + { + if ($fillStyleXml->gradientFill) { + /** @var SimpleXMLElement $gradientFill */ + $gradientFill = $fillStyleXml->gradientFill[0]; + $attr = $this->getStyleAttributes($gradientFill); + if (!empty($attr['type'])) { + $fillStyle->setFillType((string) $attr['type']); + } + $fillStyle->setRotation((float) ($attr['degree'])); + $gradientFill->registerXPathNamespace('sml', Namespaces::MAIN); + $fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); //* @phpstan-ignore-line + $fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); //* @phpstan-ignore-line + } elseif ($fillStyleXml->patternFill) { + $defaultFillStyle = ($fillStyle->getFillType() !== null) ? Fill::FILL_NONE : ''; + $fgFound = false; + $bgFound = false; + if ($fillStyleXml->patternFill->fgColor) { + $fillStyle->getStartColor()->setARGB($this->readColor($fillStyleXml->patternFill->fgColor, true)); + if ($fillStyle->getFillType() !== null) { + $defaultFillStyle = Fill::FILL_SOLID; + } + $fgFound = true; + } + if ($fillStyleXml->patternFill->bgColor) { + $fillStyle->getEndColor()->setARGB($this->readColor($fillStyleXml->patternFill->bgColor, true)); + if ($fillStyle->getFillType() !== null) { + $defaultFillStyle = Fill::FILL_SOLID; + } + $bgFound = true; + } + + $type = ''; + if ((string) $fillStyleXml->patternFill['patternType'] !== '') { + $type = (string) $fillStyleXml->patternFill['patternType']; + } else { + $attr = $this->getStyleAttributes($fillStyleXml->patternFill); + $type = (string) $attr['patternType']; + } + $patternType = ($type === '') ? $defaultFillStyle : $type; + + $fillStyle->setFillType($patternType); + if ( + !$fgFound // no foreground color specified + && !in_array($patternType, [Fill::FILL_NONE, Fill::FILL_SOLID], true) // these patterns aren't relevant + && $fillStyle->getStartColor()->getARGB() // not conditional + ) { + $fillStyle->getStartColor() + ->setARGB('', true); + } + if ( + !$bgFound // no background color specified + && !in_array($patternType, [Fill::FILL_NONE, Fill::FILL_SOLID], true) // these patterns aren't relevant + && $fillStyle->getEndColor()->getARGB() // not conditional + ) { + $fillStyle->getEndColor() + ->setARGB('', true); + } + } + } + + public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void + { + $diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp'); + $diagonalUp = self::boolean($diagonalUp); + $diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown'); + $diagonalDown = self::boolean($diagonalDown); + if ($diagonalUp === false) { + if ($diagonalDown === false) { + $borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE); + } else { + $borderStyle->setDiagonalDirection(Borders::DIAGONAL_DOWN); + } + } elseif ($diagonalDown === false) { + $borderStyle->setDiagonalDirection(Borders::DIAGONAL_UP); + } else { + $borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH); + } + + if (isset($borderStyleXml->left)) { + $this->readBorder($borderStyle->getLeft(), $borderStyleXml->left); + } + if (isset($borderStyleXml->right)) { + $this->readBorder($borderStyle->getRight(), $borderStyleXml->right); + } + if (isset($borderStyleXml->top)) { + $this->readBorder($borderStyle->getTop(), $borderStyleXml->top); + } + if (isset($borderStyleXml->bottom)) { + $this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom); + } + if (isset($borderStyleXml->diagonal)) { + $this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal); + } + } + + private function getAttribute(SimpleXMLElement $xml, string $attribute): string + { + $style = ''; + if ((string) $xml[$attribute] !== '') { + $style = (string) $xml[$attribute]; + } else { + $attr = $this->getStyleAttributes($xml); + if (isset($attr[$attribute])) { + $style = (string) $attr[$attribute]; + } + } + + return $style; + } + + private function readBorder(Border $border, SimpleXMLElement $borderXml): void + { + $style = $this->getAttribute($borderXml, 'style'); + if ($style !== '') { + $border->setBorderStyle((string) $style); + } else { + $border->setBorderStyle(Border::BORDER_NONE); + } + if (isset($borderXml->color)) { + $border->getColor()->setARGB($this->readColor($borderXml->color)); + } + } + + public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void + { + $horizontal = (string) $this->getAttribute($alignmentXml, 'horizontal'); + if ($horizontal !== '') { + $alignment->setHorizontal($horizontal); + } + $justifyLastLine = (string) $this->getAttribute($alignmentXml, 'justifyLastLine'); + if ($justifyLastLine !== '') { + $alignment->setJustifyLastLine( + self::boolean($justifyLastLine) + ); + } + $vertical = (string) $this->getAttribute($alignmentXml, 'vertical'); + if ($vertical !== '') { + $alignment->setVertical($vertical); + } + + $textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation'); + if ($textRotation > 90) { + $textRotation = 90 - $textRotation; + } + $alignment->setTextRotation($textRotation); + + $wrapText = $this->getAttribute($alignmentXml, 'wrapText'); + $alignment->setWrapText(self::boolean((string) $wrapText)); + $shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit'); + $alignment->setShrinkToFit(self::boolean((string) $shrinkToFit)); + $indent = (int) $this->getAttribute($alignmentXml, 'indent'); + $alignment->setIndent(max($indent, 0)); + $readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder'); + $alignment->setReadOrder(max($readingOrder, 0)); + } + + private static function formatGeneral(string $formatString): string + { + if ($formatString === 'GENERAL') { + $formatString = NumberFormat::FORMAT_GENERAL; + } + + return $formatString; + } + + /** + * Read style. + */ + public function readStyle(Style $docStyle, SimpleXMLElement|stdClass $style): void + { + if ($style instanceof SimpleXMLElement) { + $this->readNumberFormat($docStyle->getNumberFormat(), $style->numFmt); + } else { + /** @var SimpleXMLElement */ + $temp = $style->numFmt; + $docStyle->getNumberFormat()->setFormatCode(self::formatGeneral((string) $temp)); + } + + /** @var SimpleXMLElement $style */ + if (isset($style->font)) { + $this->readFontStyle($docStyle->getFont(), $style->font); + } + + if (isset($style->fill)) { + $this->readFillStyle($docStyle->getFill(), $style->fill); + } + + if (isset($style->border)) { + $this->readBorderStyle($docStyle->getBorders(), $style->border); + } + + if (isset($style->alignment)) { + $this->readAlignmentStyle($docStyle->getAlignment(), $style->alignment); + } + + // protection + if (isset($style->protection)) { + $this->readProtectionLocked($docStyle, $style->protection); + $this->readProtectionHidden($docStyle, $style->protection); + } + + // top-level style settings + if (isset($style->quotePrefix)) { + $docStyle->setQuotePrefix((bool) $style->quotePrefix); + } + } + + /** + * Read protection locked attribute. + */ + public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void + { + $locked = ''; + if ((string) $style['locked'] !== '') { + $locked = (string) $style['locked']; + } else { + $attr = $this->getStyleAttributes($style); + if (isset($attr['locked'])) { + $locked = (string) $attr['locked']; + } + } + if ($locked !== '') { + if (self::boolean($locked)) { + $docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED); + } else { + $docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED); + } + } + } + + /** + * Read protection hidden attribute. + */ + public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void + { + $hidden = ''; + if ((string) $style['hidden'] !== '') { + $hidden = (string) $style['hidden']; + } else { + $attr = $this->getStyleAttributes($style); + if (isset($attr['hidden'])) { + $hidden = (string) $attr['hidden']; + } + } + if ($hidden !== '') { + if (self::boolean((string) $hidden)) { + $docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED); + } else { + $docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED); + } + } + } + + public function readColorTheme(SimpleXMLElement $color): int + { + $attr = $this->getStyleAttributes($color); + $retVal = -1; + if (isset($attr['theme']) && is_numeric((string) $attr['theme']) && !isset($attr['tint'])) { + $retVal = (int) $attr['theme']; + } + + return $retVal; + } + + public function readColor(SimpleXMLElement $color, bool $background = false): string + { + $attr = $this->getStyleAttributes($color); + if (isset($attr['rgb'])) { + return (string) $attr['rgb']; + } + if (isset($attr['indexed'])) { + $indexedColor = (int) $attr['indexed']; + if ($indexedColor >= count($this->workbookPalette)) { + return Color::indexedColor($indexedColor - 7, $background)->getARGB() ?? ''; + } + + return Color::indexedColor($indexedColor, $background, $this->workbookPalette)->getARGB() ?? ''; + } + if (isset($attr['theme'])) { + if ($this->theme !== null) { + $returnColour = $this->theme->getColourByIndex((int) $attr['theme']); + if (isset($attr['tint'])) { + $tintAdjust = (float) $attr['tint']; + $returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust); + } + + return 'FF' . $returnColour; + } + } + + return ($background) ? 'FFFFFFFF' : 'FF000000'; + } + + /** @return Style[] */ + public function dxfs(bool $readDataOnly = false): array + { + $dxfs = []; + if (!$readDataOnly && $this->styleXml) { + // Conditional Styles + if ($this->styleXml->dxfs) { + foreach ($this->styleXml->dxfs->dxf as $dxf) { + $style = new Style(false, true); + $this->readStyle($style, $dxf); + $dxfs[] = $style; + } + } + // Cell Styles + if ($this->styleXml->cellStyles) { + foreach ($this->styleXml->cellStyles->cellStyle as $cellStylex) { + $cellStyle = Xlsx::getAttributes($cellStylex); + if ((int) ($cellStyle['builtinId']) == 0) { + if (isset($this->cellStyles[(int) ($cellStyle['xfId'])])) { + // Set default style + $style = new Style(); + $this->readStyle($style, $this->cellStyles[(int) ($cellStyle['xfId'])]); + + // normal style, currently not using it for anything + } + } + } + } + } + + return $dxfs; + } + + /** @return TableDxfsStyle[] */ + public function tableStyles(bool $readDataOnly = false): array + { + $tableStyles = []; + if (!$readDataOnly && $this->styleXml) { + // Conditional Styles + if ($this->styleXml->tableStyles) { + foreach ($this->styleXml->tableStyles->tableStyle as $s) { + $attrs = Xlsx::getAttributes($s); + if (isset($attrs['name'][0])) { + $style = new TableDxfsStyle((string) ($attrs['name'][0])); + foreach ($s->tableStyleElement as $e) { + $a = Xlsx::getAttributes($e); + if (isset($a['dxfId'][0], $a['type'][0])) { + switch ($a['type'][0]) { + case 'headerRow': + $style->setHeaderRow((int) ($a['dxfId'][0])); + + break; + case 'firstRowStripe': + $style->setFirstRowStripe((int) ($a['dxfId'][0])); + + break; + case 'secondRowStripe': + $style->setSecondRowStripe((int) ($a['dxfId'][0])); + + break; + default: + } + } + } + $tableStyles[] = $style; + } + } + } + } + + return $tableStyles; + } + + /** @return mixed[] */ + public function styles(): array + { + return $this->styles; + } + + /** + * Get array item. + * + * @param false|mixed[] $array (usually array, in theory can be false) + */ + private static function getArrayItem(mixed $array): ?SimpleXMLElement + { + return is_array($array) ? ($array[0] ?? null) : null; // @phpstan-ignore-line + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php new file mode 100644 index 0000000..121697c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -0,0 +1,137 @@ +worksheet = $workSheet; + $this->tableXml = $tableXml; + } + + /** + * Loads Table into the Worksheet. + * + * @param TableDxfsStyle[] $tableStyles + * @param Style[] $dxfs + */ + public function load(array $tableStyles, array $dxfs): void + { + $this->tableAttributes = $this->tableXml->attributes() ?? []; + // Remove all "$" in the table range + $tableRange = (string) preg_replace('/\$/', '', $this->tableAttributes['ref'] ?? ''); + if (str_contains($tableRange, ':')) { + $this->readTable($tableRange, $tableStyles, $dxfs); + } + } + + /** + * Read Table from xml. + * + * @param TableDxfsStyle[] $tableStyles + * @param Style[] $dxfs + */ + private function readTable(string $tableRange, array $tableStyles, array $dxfs): void + { + $table = new Table($tableRange); + /** @var string[] */ + $attributes = $this->tableAttributes; + $table->setName((string) ($attributes['displayName'] ?? '')); + $table->setShowHeaderRow(((string) ($attributes['headerRowCount'] ?? '')) !== '0'); + $table->setShowTotalsRow(((string) ($attributes['totalsRowCount'] ?? '')) === '1'); + + $this->readTableAutoFilter($table, $this->tableXml->autoFilter); + $this->readTableColumns($table, $this->tableXml->tableColumns); + $this->readTableStyle($table, $this->tableXml->tableStyleInfo, $tableStyles, $dxfs); + + (new AutoFilter($table, $this->tableXml))->load(); + $this->worksheet->addTable($table); + } + + /** + * Reads TableAutoFilter from xml. + */ + private function readTableAutoFilter(Table $table, SimpleXMLElement $autoFilterXml): void + { + if ($autoFilterXml->filterColumn === null) { + $table->setAllowFilter(false); + + return; + } + + foreach ($autoFilterXml->filterColumn as $filterColumn) { + /** @var SimpleXMLElement */ + $attributes = $filterColumn->attributes() ?? ['colId' => 0, 'hiddenButton' => 0]; + $column = $table->getColumnByOffset((int) $attributes['colId']); + $column->setShowFilterButton(((string) $attributes['hiddenButton']) !== '1'); + } + } + + /** + * Reads TableColumns from xml. + */ + private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXml): void + { + $offset = 0; + foreach ($tableColumnsXml->tableColumn as $tableColumn) { + /** @var SimpleXMLElement */ + $attributes = $tableColumn->attributes() ?? ['totalsRowLabel' => 0, 'totalsRowFunction' => 0]; + $column = $table->getColumnByOffset($offset++); + + if ($table->getShowTotalsRow()) { + if ($attributes['totalsRowLabel']) { + $column->setTotalsRowLabel((string) $attributes['totalsRowLabel']); + } + + if ($attributes['totalsRowFunction']) { + $column->setTotalsRowFunction((string) $attributes['totalsRowFunction']); + } + } + + if ($tableColumn->calculatedColumnFormula) { + $column->setColumnFormula((string) $tableColumn->calculatedColumnFormula); + } + } + } + + /** + * Reads TableStyle from xml. + * + * @param TableDxfsStyle[] $tableStyles + * @param Style[] $dxfs + */ + private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml, array $tableStyles, array $dxfs): void + { + $tableStyle = new TableStyle(); + $attributes = $tableStyleInfoXml->attributes(); + if ($attributes !== null) { + $tableStyle->setTheme((string) $attributes['name']); + $tableStyle->setShowRowStripes((string) $attributes['showRowStripes'] === '1'); + $tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1'); + $tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1'); + $tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1'); + + foreach ($tableStyles as $style) { + if ($style->getName() === (string) $attributes['name']) { + $tableStyle->setTableDxfsStyle($style, $dxfs); + } + } + } + $table->setStyle($tableStyle); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Theme.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Theme.php new file mode 100644 index 0000000..4a31c3e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/Theme.php @@ -0,0 +1,64 @@ +themeName = $themeName; + $this->colourSchemeName = $colourSchemeName; + $this->colourMap = $colourMap; + } + + /** + * Not called by Reader, never accessible any other time. + * + * @codeCoverageIgnore + */ + public function getThemeName(): string + { + return $this->themeName; + } + + /** + * Not called by Reader, never accessible any other time. + * + * @codeCoverageIgnore + */ + public function getColourSchemeName(): string + { + return $this->colourSchemeName; + } + + /** + * Get colour Map Value by Position. + */ + public function getColourByIndex(int $index): ?string + { + return $this->colourMap[$index] ?? null; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php new file mode 100644 index 0000000..10f26e9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php @@ -0,0 +1,142 @@ +spreadsheet = $spreadsheet; + } + + /** @param array $mapSheetId */ + public function viewSettings(SimpleXMLElement $xmlWorkbook, string $mainNS, array $mapSheetId, bool $readDataOnly): void + { + // Default active sheet index to the first loaded worksheet from the file + $this->spreadsheet->setActiveSheetIndex(0); + + $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView; + if ($readDataOnly !== true && !empty($workbookView)) { + $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView)); + // active sheet index + $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index + // keep active sheet index if sheet is still loaded, else first sheet is set as the active worksheet + if (isset($mapSheetId[$activeTab])) { + $this->spreadsheet->setActiveSheetIndex($mapSheetId[$activeTab]); + } + + $this->horizontalScroll($workbookViewAttributes); + $this->verticalScroll($workbookViewAttributes); + $this->sheetTabs($workbookViewAttributes); + $this->minimized($workbookViewAttributes); + $this->autoFilterDateGrouping($workbookViewAttributes); + $this->firstSheet($workbookViewAttributes); + $this->visibility($workbookViewAttributes); + $this->tabRatio($workbookViewAttributes); + } + } + + public static function testSimpleXml(mixed $value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) + ? $value + : new SimpleXMLElement(''); + } + + public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement + { + return self::testSimpleXml($value === null ? $value : $value->attributes($ns)); + } + + /** + * Convert an 'xsd:boolean' XML value to a PHP boolean value. + * A valid 'xsd:boolean' XML value can be one of the following + * four values: 'true', 'false', '1', '0'. It is case-sensitive. + * + * Note that just doing '(bool) $xsdBoolean' is not safe, + * since '(bool) "false"' returns true. + * + * @see https://www.w3.org/TR/xmlschema11-2/#boolean + * + * @param string $xsdBoolean An XML string value of type 'xsd:boolean' + * + * @return bool Boolean value + */ + private function castXsdBooleanToBool(string $xsdBoolean): bool + { + if ($xsdBoolean === 'false') { + return false; + } + + return (bool) $xsdBoolean; + } + + private function horizontalScroll(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->showHorizontalScroll)) { + $showHorizontalScroll = (string) $workbookViewAttributes->showHorizontalScroll; + $this->spreadsheet->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll)); + } + } + + private function verticalScroll(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->showVerticalScroll)) { + $showVerticalScroll = (string) $workbookViewAttributes->showVerticalScroll; + $this->spreadsheet->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll)); + } + } + + private function sheetTabs(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->showSheetTabs)) { + $showSheetTabs = (string) $workbookViewAttributes->showSheetTabs; + $this->spreadsheet->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs)); + } + } + + private function minimized(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->minimized)) { + $minimized = (string) $workbookViewAttributes->minimized; + $this->spreadsheet->setMinimized($this->castXsdBooleanToBool($minimized)); + } + } + + private function autoFilterDateGrouping(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->autoFilterDateGrouping)) { + $autoFilterDateGrouping = (string) $workbookViewAttributes->autoFilterDateGrouping; + $this->spreadsheet->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping)); + } + } + + private function firstSheet(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->firstSheet)) { + $firstSheet = (string) $workbookViewAttributes->firstSheet; + $this->spreadsheet->setFirstSheetIndex((int) $firstSheet); + } + } + + private function visibility(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->visibility)) { + $visibility = (string) $workbookViewAttributes->visibility; + $this->spreadsheet->setVisibility($visibility); + } + } + + private function tabRatio(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->tabRatio)) { + $tabRatio = (string) $workbookViewAttributes->tabRatio; + $this->spreadsheet->setTabRatio((int) $tabRatio); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml.php new file mode 100644 index 0000000..08aa7f4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml.php @@ -0,0 +1,734 @@ +securityScanner = XmlScanner::getInstance($this); + /** @var callable */ + $unentity = [self::class, 'unentity']; + $this->securityScanner->setAdditionalCallback($unentity); + } + + public static function unentity(string $contents): string + { + $contents = preg_replace('/&(amp|lt|gt|quot|apos);/', "\u{fffe}\u{feff}\$1;", trim($contents)) ?? $contents; + $contents = html_entity_decode($contents, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8'); + $contents = str_replace("\u{fffe}\u{feff}", '&', $contents); + + return $contents; + } + + private string $fileContents = ''; + + private string $xmlFailMessage = ''; + + /** @return mixed[] */ + public static function xmlMappings(): array + { + return array_merge( + Style\Fill::FILL_MAPPINGS, + Style\Border::BORDER_MAPPINGS + ); + } + + /** + * Can the current IReader read the file? + */ + public function canRead(string $filename): bool + { + // Office xmlns:o="urn:schemas-microsoft-com:office:office" + // Excel xmlns:x="urn:schemas-microsoft-com:office:excel" + // XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" + // Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet" + // XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" + // XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" + // MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset" + // Rowset xmlns:z="#RowsetSchema" + // + + $signature = [ + 'getSecurityScannerOrThrow()->scan($data); + + // Why? + //$data = str_replace("'", '"', $data); // fix headers with single quote + + $valid = true; + foreach ($signature as $match) { + // every part of the signature must be present + if (!str_contains($data, $match)) { + $valid = false; + + break; + } + } + + $this->fileContents = $data; + + return $valid; + } + + /** @return false|SimpleXMLElement */ + private function trySimpleXMLLoadStringPrivate(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool + { + $this->xmlFailMessage = "Cannot load invalid XML $fileOrString: " . $filename; + $xml = false; + + try { + $data = $this->fileContents; + $continue = true; + if ($data === '' && $fileOrString === 'file') { + if ($filename === '') { + $this->xmlFailMessage = 'Cannot load empty path'; + $continue = false; + } else { + $datax = @file_get_contents($filename); + $data = $datax ?: ''; + $continue = $datax !== false; + } + } + if ($continue) { + $xml = @simplexml_load_string( + $this->getSecurityScannerOrThrow() + ->scan($data) + ); + } + } catch (Throwable $e) { + throw new Exception($this->xmlFailMessage, 0, $e); + } + $this->fileContents = ''; + + return $xml; + } + + /** + * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. + * + * @return string[] + */ + public function listWorksheetNames(string $filename): array + { + File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an Invalid Spreadsheet file.'); + } + + $worksheetNames = []; + + $xml = $this->trySimpleXMLLoadStringPrivate($filename); + if ($xml === false) { + throw new Exception("Problem reading {$filename}"); + } + + $xml_ss = $xml->children(self::NAMESPACES_SS); + foreach ($xml_ss->Worksheet as $worksheet) { + $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS); + $worksheetNames[] = (string) $worksheet_ss['Name']; + } + + return $worksheetNames; + } + + /** + * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). + * + * @return array + */ + public function listWorksheetInfo(string $filename): array + { + File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an Invalid Spreadsheet file.'); + } + + $worksheetInfo = []; + + $xml = $this->trySimpleXMLLoadStringPrivate($filename); + if ($xml === false) { + throw new Exception("Problem reading {$filename}"); + } + + $worksheetID = 1; + $xml_ss = $xml->children(self::NAMESPACES_SS); + foreach ($xml_ss->Worksheet as $worksheet) { + $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS); + + $tmpInfo = []; + $tmpInfo['worksheetName'] = ''; + $tmpInfo['lastColumnLetter'] = 'A'; + $tmpInfo['lastColumnIndex'] = 0; + $tmpInfo['totalRows'] = 0; + $tmpInfo['totalColumns'] = 0; + + $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}"; + if (isset($worksheet_ss['Name'])) { + $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name']; + } + + if (isset($worksheet->Table->Row)) { + $rowIndex = 0; + + foreach ($worksheet->Table->Row as $rowData) { + $columnIndex = 0; + $rowHasData = false; + + foreach ($rowData->Cell as $cell) { + if (isset($cell->Data)) { + $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); + $rowHasData = true; + } + + ++$columnIndex; + } + + ++$rowIndex; + + if ($rowHasData) { + $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); + } + } + } + + $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); + $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; + $tmpInfo['sheetState'] = Worksheet::SHEETSTATE_VISIBLE; + + $worksheetInfo[] = $tmpInfo; + ++$worksheetID; + } + + return $worksheetInfo; + } + + /** + * Loads Spreadsheet from string. + */ + public function loadSpreadsheetFromString(string $contents): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + $spreadsheet->removeSheetByIndex(0); + + // Load into this instance + return $this->loadIntoExisting($contents, $spreadsheet, true); + } + + /** + * Loads Spreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + $spreadsheet = $this->newSpreadsheet(); + $spreadsheet->setValueBinder($this->valueBinder); + $spreadsheet->removeSheetByIndex(0); + + // Load into this instance + return $this->loadIntoExisting($filename, $spreadsheet); + } + + /** + * Loads from file or contents into Spreadsheet instance. + * + * @param string $filename file name if useContents is false else file contents + */ + public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, bool $useContents = false): Spreadsheet + { + if ($useContents) { + $this->fileContents = $filename; + $fileOrString = 'string'; + } else { + File::assertFile($filename); + if (!$this->canRead($filename)) { + throw new Exception($filename . ' is an Invalid Spreadsheet file.'); + } + $fileOrString = 'file'; + } + + $xml = $this->trySimpleXMLLoadStringPrivate($filename, $fileOrString); + if ($xml === false) { + throw new Exception($this->xmlFailMessage); + } + + $namespaces = $xml->getNamespaces(true); + + (new Properties($spreadsheet))->readProperties($xml, $namespaces); + + $this->styles = (new Style())->parseStyles($xml, $namespaces); + if (isset($this->styles['Default']) && is_array($this->styles['Default'])) { + $spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']); + } + + $worksheetID = 0; + $xml_ss = $xml->children(self::NAMESPACES_SS); + + $sheetCreated = false; + /** @var null|SimpleXMLElement $worksheetx */ + foreach ($xml_ss->Worksheet as $worksheetx) { + $worksheet = $worksheetx ?? new SimpleXMLElement(''); + $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS); + + if ( + isset($this->loadSheetsOnly, $worksheet_ss['Name']) + && (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly)) + ) { + continue; + } + + // Create new Worksheet + $spreadsheet->createSheet(); + $sheetCreated = true; + $spreadsheet->setActiveSheetIndex($worksheetID); + $worksheetName = ''; + if (isset($worksheet_ss['Name'])) { + $worksheetName = (string) $worksheet_ss['Name']; + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in + // formula cells... during the load, all formulae should be correct, and we're simply bringing + // the worksheet name in line with the formula, not the reverse + $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + } + if (isset($worksheet_ss['Protected'])) { + $protection = (string) $worksheet_ss['Protected'] === '1'; + $spreadsheet->getActiveSheet()->getProtection()->setSheet($protection); + } + + // locally scoped defined names + if (isset($worksheet->Names[0])) { + foreach ($worksheet->Names[0] as $definedName) { + $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS); + $name = (string) $definedName_ss['Name']; + $definedValue = (string) $definedName_ss['RefersTo']; + $convertedValue = AddressHelper::convertFormulaToA1($definedValue); + if ($convertedValue[0] === '=') { + $convertedValue = substr($convertedValue, 1); + } + $spreadsheet->addDefinedName(DefinedName::createInstance($name, $spreadsheet->getActiveSheet(), $convertedValue, true)); + } + } + + $columnID = 'A'; + if (isset($worksheet->Table->Column)) { + foreach ($worksheet->Table->Column as $columnData) { + $columnData_ss = self::getAttributes($columnData, self::NAMESPACES_SS); + $colspan = 0; + if (isset($columnData_ss['Span'])) { + $spanAttr = (string) $columnData_ss['Span']; + if (is_numeric($spanAttr)) { + $colspan = max(0, (int) $spanAttr); + } + } + if (isset($columnData_ss['Index'])) { + $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']); + } + $columnWidth = null; + if (isset($columnData_ss['Width'])) { + $columnWidth = $columnData_ss['Width']; + } + $columnVisible = null; + if (isset($columnData_ss['Hidden'])) { + $columnVisible = ((string) $columnData_ss['Hidden']) !== '1'; + } + while ($colspan >= 0) { + /** @var string $columnID */ + if (isset($columnWidth)) { + $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4); + } + if (isset($columnVisible)) { + $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible); + } + StringHelper::stringIncrement($columnID); + --$colspan; + } + } + } + + $rowID = 1; + if (isset($worksheet->Table->Row)) { + $additionalMergedCells = 0; + foreach ($worksheet->Table->Row as $rowData) { + $rowHasData = false; + $row_ss = self::getAttributes($rowData, self::NAMESPACES_SS); + if (isset($row_ss['Index'])) { + $rowID = (int) $row_ss['Index']; + } + if (isset($row_ss['Hidden'])) { + $rowVisible = ((string) $row_ss['Hidden']) !== '1'; + $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible); + } + + $columnID = 'A'; + foreach ($rowData->Cell as $cell) { + $arrayRef = ''; + $cell_ss = self::getAttributes($cell, self::NAMESPACES_SS); + if (isset($cell_ss['Index'])) { + $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']); + } + $cellRange = $columnID . $rowID; + if (isset($cell_ss['ArrayRange'])) { + $arrayRange = (string) $cell_ss['ArrayRange']; + $arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, Coordinate::columnIndexFromString($columnID)); + } + + if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + StringHelper::stringIncrement($columnID); + + continue; + } + + if (isset($cell_ss['HRef'])) { + $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl((string) $cell_ss['HRef']); + } + + if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) { + $columnTo = $columnID; + if (isset($cell_ss['MergeAcross'])) { + $additionalMergedCells += (int) $cell_ss['MergeAcross']; + $columnTo = Coordinate::stringFromColumnIndex((int) (Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross'])); + } + $rowTo = $rowID; + if (isset($cell_ss['MergeDown'])) { + $rowTo = $rowTo + $cell_ss['MergeDown']; + } + $cellRange .= ':' . $columnTo . $rowTo; + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + } + + $hasCalculatedValue = false; + $cellDataFormula = ''; + if (isset($cell_ss['Formula'])) { + $cellDataFormula = $cell_ss['Formula']; + $hasCalculatedValue = true; + if ($arrayRef !== '') { + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayRef]); + } + } + if (isset($cell->Data)) { + $cellData = $cell->Data; + $cellValue = (string) $cellData; + $type = DataType::TYPE_NULL; + $cellData_ss = self::getAttributes($cellData, self::NAMESPACES_SS); + if (isset($cellData_ss['Type'])) { + $cellDataType = $cellData_ss['Type']; + switch ($cellDataType) { + /* + const TYPE_STRING = 's'; + const TYPE_FORMULA = 'f'; + const TYPE_NUMERIC = 'n'; + const TYPE_BOOL = 'b'; + const TYPE_NULL = 'null'; + const TYPE_INLINE = 'inlineStr'; + const TYPE_ERROR = 'e'; + */ + case 'String': + $type = DataType::TYPE_STRING; + $rich = $cellData->children('http://www.w3.org/TR/REC-html40'); + if ($rich) { + // in case of HTML content we extract the payload + // and convert it into a rich text object + $content = $cellData->asXML() ?: ''; + $html = new HelperHtml(); + $cellValue = $html->toRichTextObject($content, true); + } + + break; + case 'Number': + $type = DataType::TYPE_NUMERIC; + $cellValue = (float) $cellValue; + if (floor($cellValue) == $cellValue) { + $cellValue = (int) $cellValue; + } + + break; + case 'Boolean': + $type = DataType::TYPE_BOOL; + $cellValue = ($cellValue != 0); + + break; + case 'DateTime': + $type = DataType::TYPE_NUMERIC; + $dateTime = new DateTime($cellValue, new DateTimeZone('UTC')); + $cellValue = Date::PHPToExcel($dateTime); + + break; + case 'Error': + $type = DataType::TYPE_ERROR; + $hasCalculatedValue = false; + + break; + } + } + + $originalType = $type; + if ($hasCalculatedValue) { + $type = DataType::TYPE_FORMULA; + $columnNumber = Coordinate::columnIndexFromString($columnID); + $cellDataFormula = AddressHelper::convertFormulaToA1($cellDataFormula, $rowID, $columnNumber); + } + + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type); + if ($hasCalculatedValue) { + $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue, $originalType === DataType::TYPE_NUMERIC); + } + $rowHasData = true; + } + + if (isset($cell->Comment)) { + $this->parseCellComment($cell->Comment, $spreadsheet, $columnID, $rowID); + } + + if (isset($cell_ss['StyleID'])) { + $style = (string) $cell_ss['StyleID']; + if ((isset($this->styles[$style])) && is_array($this->styles[$style]) && (!empty($this->styles[$style]))) { + $spreadsheet->getActiveSheet()->getStyle($cellRange) + ->applyFromArray($this->styles[$style]); + } + } + StringHelper::stringIncrement($columnID); + while ($additionalMergedCells > 0) { + StringHelper::stringIncrement($columnID); + --$additionalMergedCells; + } + } + + if ($rowHasData) { + if (isset($row_ss['Height'])) { + $rowHeight = $row_ss['Height']; + $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight((float) $rowHeight); + } + } + + ++$rowID; + } + } + + $dataValidations = new Xml\DataValidations(); + $dataValidations->loadDataValidations($worksheet, $spreadsheet); + $xmlX = $worksheet->children(Namespaces::URN_EXCEL); + if (isset($xmlX->WorksheetOptions)) { + if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) { + $spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW); + } + if (isset($xmlX->WorksheetOptions->Zoom)) { + $zoomScaleNormal = (int) $xmlX->WorksheetOptions->Zoom; + if ($zoomScaleNormal > 0) { + $spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleNormal($zoomScaleNormal); + $spreadsheet->getActiveSheet()->getSheetView()->setZoomScale($zoomScaleNormal); + } + } + if (isset($xmlX->WorksheetOptions->PageBreakZoom)) { + $zoomScaleNormal = (int) $xmlX->WorksheetOptions->PageBreakZoom; + if ($zoomScaleNormal > 0) { + $spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleSheetLayoutView($zoomScaleNormal); + } + } + if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) { + $spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW); + } + if (isset($xmlX->WorksheetOptions->FreezePanes)) { + $freezeRow = $freezeColumn = 1; + if (isset($xmlX->WorksheetOptions->SplitHorizontal)) { + $freezeRow = (int) $xmlX->WorksheetOptions->SplitHorizontal + 1; + } + if (isset($xmlX->WorksheetOptions->SplitVertical)) { + $freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1; + } + $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowBottomPane; + $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnRightPane; + if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) { + $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1); + $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, $leftTopCoordinate, !isset($xmlX->WorksheetOptions->FrozenNoSplit)); + } else { + $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, null, !isset($xmlX->WorksheetOptions->FrozenNoSplit)); + } + } elseif (isset($xmlX->WorksheetOptions->SplitVertical) || isset($xmlX->WorksheetOptions->SplitHorizontal)) { + if (isset($xmlX->WorksheetOptions->SplitHorizontal)) { + $ySplit = (int) $xmlX->WorksheetOptions->SplitHorizontal; + $spreadsheet->getActiveSheet()->setYSplit($ySplit); + } + if (isset($xmlX->WorksheetOptions->SplitVertical)) { + $xSplit = (int) $xmlX->WorksheetOptions->SplitVertical; + $spreadsheet->getActiveSheet()->setXSplit($xSplit); + } + if (isset($xmlX->WorksheetOptions->LeftColumnVisible) || isset($xmlX->WorksheetOptions->TopRowVisible)) { + $leftTopColumn = $leftTopRow = 1; + if (isset($xmlX->WorksheetOptions->LeftColumnVisible)) { + $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnVisible; + } + if (isset($xmlX->WorksheetOptions->TopRowVisible)) { + $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowVisible; + } + $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow"; + $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate); + } + + $leftTopColumn = $leftTopRow = 1; + if (isset($xmlX->WorksheetOptions->LeftColumnRightPane)) { + $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnRightPane; + } + if (isset($xmlX->WorksheetOptions->TopRowBottomPane)) { + $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowBottomPane; + } + $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow"; + $spreadsheet->getActiveSheet()->setPaneTopLeftCell($leftTopCoordinate); + } + (new PageSettings($xmlX))->loadPageSettings($spreadsheet); + if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) { + $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible; + $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible; + if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) { + $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1); + $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate); + } + } + $rangeCalculated = false; + if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) { + if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) { + $selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) + . $selectionMatches[1] + . ':' + . Coordinate::stringFromColumnIndex((int) $selectionMatches[4]) + . $selectionMatches[3]; + $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell); + $rangeCalculated = true; + } + } + if (!$rangeCalculated) { + if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) { + $activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow; + } else { + $activeRow = 0; + } + if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) { + $activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol; + } else { + $activeColumn = 0; + } + if (is_numeric($activeRow) && is_numeric($activeColumn)) { + $selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1); + $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell); + } + } + } + if (isset($xmlX->PageBreaks)) { + if (isset($xmlX->PageBreaks->ColBreaks)) { + foreach ($xmlX->PageBreaks->ColBreaks->ColBreak as $colBreak) { + $colBreak = (string) $colBreak->Column; + $spreadsheet->getActiveSheet()->setBreak([1 + (int) $colBreak, 1], Worksheet::BREAK_COLUMN); + } + } + if (isset($xmlX->PageBreaks->RowBreaks)) { + foreach ($xmlX->PageBreaks->RowBreaks->RowBreak as $rowBreak) { + $rowBreak = (string) $rowBreak->Row; + $spreadsheet->getActiveSheet()->setBreak([1, (int) $rowBreak], Worksheet::BREAK_ROW); + } + } + } + ++$worksheetID; + } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $spreadsheet->createSheet(); + } + + // Globally scoped defined names + $activeSheetIndex = 0; + if (isset($xml->ExcelWorkbook->ActiveSheet)) { + $activeSheetIndex = (int) (string) $xml->ExcelWorkbook->ActiveSheet; + } + $activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex); + if (isset($xml->Names[0])) { + foreach ($xml->Names[0] as $definedName) { + $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS); + $name = (string) $definedName_ss['Name']; + $definedValue = (string) $definedName_ss['RefersTo']; + $convertedValue = AddressHelper::convertFormulaToA1($definedValue); + if ($convertedValue[0] === '=') { + $convertedValue = substr($convertedValue, 1); + } + $spreadsheet->addDefinedName(DefinedName::createInstance($name, $activeWorksheet, $convertedValue)); + } + } + + // Return + return $spreadsheet; + } + + protected function parseCellComment( + SimpleXMLElement $comment, + Spreadsheet $spreadsheet, + string $columnID, + int $rowID + ): void { + $commentAttributes = $comment->attributes(self::NAMESPACES_SS); + $author = 'unknown'; + if (isset($commentAttributes->Author)) { + $author = (string) $commentAttributes->Author; + } + + $node = $comment->Data->asXML(); + $annotation = strip_tags((string) $node); + $spreadsheet->getActiveSheet()->getComment($columnID . $rowID) + ->setAuthor($author) + ->setText($this->parseRichText($annotation)); + } + + protected function parseRichText(string $annotation): RichText + { + $value = new RichText(); + + $value->createText($annotation); + + return $value; + } + + private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement + { + return ($simple === null) + ? new SimpleXMLElement('') + : ($simple->attributes($node) ?? new SimpleXMLElement('')); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/DataValidations.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/DataValidations.php new file mode 100644 index 0000000..74d7ec4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/DataValidations.php @@ -0,0 +1,178 @@ + DataValidation::OPERATOR_BETWEEN, + 'equal' => DataValidation::OPERATOR_EQUAL, + 'greater' => DataValidation::OPERATOR_GREATERTHAN, + 'greaterorequal' => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 'less' => DataValidation::OPERATOR_LESSTHAN, + 'lessorequal' => DataValidation::OPERATOR_LESSTHANOREQUAL, + 'notbetween' => DataValidation::OPERATOR_NOTBETWEEN, + 'notequal' => DataValidation::OPERATOR_NOTEQUAL, + ]; + + private const TYPE_MAPPINGS = [ + 'textlength' => DataValidation::TYPE_TEXTLENGTH, + ]; + + private int $thisRow = 0; + + private int $thisColumn = 0; + + /** @param string[] $matches */ + private function replaceR1C1(array $matches): string + { + return AddressHelper::convertToA1($matches[0], $this->thisRow, $this->thisColumn, false); + } + + public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $spreadsheet): void + { + $xmlX = $worksheet->children(Namespaces::URN_EXCEL); + $sheet = $spreadsheet->getActiveSheet(); + /** @var callable $pregCallback */ + $pregCallback = [$this, 'replaceR1C1']; + foreach ($xmlX->DataValidation as $dataValidation) { + $combinedCells = ''; + $separator = ''; + $validation = new DataValidation(); + + // set defaults + $validation->setShowDropDown(true); + $validation->setShowInputMessage(true); + $validation->setShowErrorMessage(true); + $validation->setShowDropDown(true); + $this->thisRow = 1; + $this->thisColumn = 1; + + foreach ($dataValidation as $tagName => $tagValue) { + $tagValue = (string) $tagValue; + $tagValueLower = strtolower($tagValue); + switch ($tagName) { + case 'Range': + foreach (explode(',', $tagValue) as $range) { + $cell = ''; + if (preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) { + // range + $firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) + . $selectionMatches[1]; + $cell = $firstCell + . ':' + . Coordinate::stringFromColumnIndex((int) $selectionMatches[4]) + . $selectionMatches[3]; + $this->thisRow = (int) $selectionMatches[1]; + $this->thisColumn = (int) $selectionMatches[2]; + $sheet->getCell($firstCell); + $combinedCells .= "$separator$cell"; + $separator = ' '; + } elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) { + // cell + $cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2]) + . $selectionMatches[1]; + $sheet->getCell($cell); + $this->thisRow = (int) $selectionMatches[1]; + $this->thisColumn = (int) $selectionMatches[2]; + $combinedCells .= "$separator$cell"; + $separator = ' '; + } elseif (preg_match('/^C(\d+)(:C(]\d+))?$/', (string) $range, $selectionMatches) === 1) { + // column + $firstCol = $selectionMatches[1]; + $firstColString = Coordinate::stringFromColumnIndex((int) $firstCol); + $lastCol = $selectionMatches[3] ?? $firstCol; + $lastColString = Coordinate::stringFromColumnIndex((int) $lastCol); + $firstCell = "{$firstColString}1"; + $cell = "$firstColString:$lastColString"; + $this->thisColumn = (int) $firstCol; + $sheet->getCell($firstCell); + $combinedCells .= "$separator$cell"; + $separator = ' '; + } elseif (preg_match('/^R(\d+)(:R(]\d+))?$/', (string) $range, $selectionMatches)) { + // row + $firstRow = $selectionMatches[1]; + $lastRow = $selectionMatches[3] ?? $firstRow; + $firstCell = "A$firstRow"; + $cell = "$firstRow:$lastRow"; + $this->thisRow = (int) $firstRow; + $sheet->getCell($firstCell); + $combinedCells .= "$separator$cell"; + $separator = ' '; + } + } + + break; + case 'Type': + $validation->setType(self::TYPE_MAPPINGS[$tagValueLower] ?? $tagValueLower); + + break; + case 'Qualifier': + $validation->setOperator(self::OPERATOR_MAPPINGS[$tagValueLower] ?? $tagValueLower); + + break; + case 'InputTitle': + $validation->setPromptTitle($tagValue); + + break; + case 'InputMessage': + $validation->setPrompt($tagValue); + + break; + case 'InputHide': + $validation->setShowInputMessage(false); + + break; + case 'ErrorStyle': + $validation->setErrorStyle($tagValueLower); + + break; + case 'ErrorTitle': + $validation->setErrorTitle($tagValue); + + break; + case 'ErrorMessage': + $validation->setError($tagValue); + + break; + case 'ErrorHide': + $validation->setShowErrorMessage(false); + + break; + case 'ComboHide': + $validation->setShowDropDown(false); + + break; + case 'UseBlank': + $validation->setAllowBlank(true); + + break; + case 'CellRangeList': + // FIXME missing FIXME + + break; + case 'Min': + case 'Value': + $tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue); + $validation->setFormula1($tagValue); + + break; + case 'Max': + $tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue); + $validation->setFormula2($tagValue); + + break; + } + } + + $sheet->setDataValidation($combinedCells, $validation); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/PageSettings.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/PageSettings.php new file mode 100644 index 0000000..2434e86 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/PageSettings.php @@ -0,0 +1,134 @@ +pageSetup($xmlX, $this->getPrintDefaults()); + $this->printSettings = $this->printSetup($xmlX, $printSettings); //* @phpstan-ignore-line + } + + public function loadPageSettings(Spreadsheet $spreadsheet): void + { + $spreadsheet->getActiveSheet()->getPageSetup() + ->setPaperSize($this->printSettings->paperSize) + ->setOrientation($this->printSettings->orientation) + ->setScale($this->printSettings->scale) + ->setVerticalCentered($this->printSettings->verticalCentered) + ->setHorizontalCentered($this->printSettings->horizontalCentered) + ->setPageOrder($this->printSettings->printOrder); + $spreadsheet->getActiveSheet()->getPageMargins() + ->setTop($this->printSettings->topMargin) + ->setHeader($this->printSettings->headerMargin) + ->setLeft($this->printSettings->leftMargin) + ->setRight($this->printSettings->rightMargin) + ->setBottom($this->printSettings->bottomMargin) + ->setFooter($this->printSettings->footerMargin); + } + + private function getPrintDefaults(): stdClass + { + return (object) [ + 'paperSize' => 9, + 'orientation' => PageSetup::ORIENTATION_DEFAULT, + 'scale' => 100, + 'horizontalCentered' => false, + 'verticalCentered' => false, + 'printOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER, + 'topMargin' => 0.75, + 'headerMargin' => 0.3, + 'leftMargin' => 0.7, + 'rightMargin' => 0.7, + 'bottomMargin' => 0.75, + 'footerMargin' => 0.3, + ]; + } + + private function pageSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass + { + if (isset($xmlX->WorksheetOptions->PageSetup)) { + foreach ($xmlX->WorksheetOptions->PageSetup as $pageSetupData) { + foreach ($pageSetupData as $pageSetupKey => $pageSetupValue) { + $pageSetupAttributes = $pageSetupValue->attributes(Namespaces::URN_EXCEL); + if ($pageSetupAttributes !== null) { + switch ($pageSetupKey) { + case 'Layout': + $this->setLayout($printDefaults, $pageSetupAttributes); + + break; + case 'Header': + $printDefaults->headerMargin = (float) $pageSetupAttributes->Margin ?: 1.0; + + break; + case 'Footer': + $printDefaults->footerMargin = (float) $pageSetupAttributes->Margin ?: 1.0; + + break; + case 'PageMargins': + $this->setMargins($printDefaults, $pageSetupAttributes); + + break; + } + } + } + } + } + + return $printDefaults; + } + + private function printSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass + { + if (isset($xmlX->WorksheetOptions->Print)) { + foreach ($xmlX->WorksheetOptions->Print as $printData) { + foreach ($printData as $printKey => $printValue) { + switch ($printKey) { + case 'LeftToRight': + $printDefaults->printOrder = PageSetup::PAGEORDER_OVER_THEN_DOWN; + + break; + case 'PaperSizeIndex': + $printDefaults->paperSize = (int) $printValue ?: 9; + + break; + case 'Scale': + $printDefaults->scale = (int) $printValue ?: 100; + + break; + } + } + } + } + + return $printDefaults; + } + + private function setLayout(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void + { + $printDefaults->orientation = (string) strtolower($pageSetupAttributes->Orientation ?? '') ?: PageSetup::ORIENTATION_PORTRAIT; + $printDefaults->horizontalCentered = (bool) $pageSetupAttributes->CenterHorizontal ?: false; + $printDefaults->verticalCentered = (bool) $pageSetupAttributes->CenterVertical ?: false; + } + + private function setMargins(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void + { + $printDefaults->leftMargin = (float) $pageSetupAttributes->Left ?: 1.0; + $printDefaults->rightMargin = (float) $pageSetupAttributes->Right ?: 1.0; + $printDefaults->topMargin = (float) $pageSetupAttributes->Top ?: 1.0; + $printDefaults->bottomMargin = (float) $pageSetupAttributes->Bottom ?: 1.0; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Properties.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Properties.php new file mode 100644 index 0000000..83116ba --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Properties.php @@ -0,0 +1,158 @@ +spreadsheet = $spreadsheet; + } + + /** @param string[] $namespaces */ + public function readProperties(SimpleXMLElement $xml, array $namespaces): void + { + $this->readStandardProperties($xml); + $this->readCustomProperties($xml, $namespaces); + } + + protected function readStandardProperties(SimpleXMLElement $xml): void + { + if (isset($xml->DocumentProperties[0])) { + $docProps = $this->spreadsheet->getProperties(); + + foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) { + $propertyValue = (string) $propertyValue; + + $this->processStandardProperty($docProps, $propertyName, $propertyValue); + } + } + } + + /** @param string[] $namespaces */ + protected function readCustomProperties(SimpleXMLElement $xml, array $namespaces): void + { + if (isset($xml->CustomDocumentProperties) && is_iterable($xml->CustomDocumentProperties[0])) { + $docProps = $this->spreadsheet->getProperties(); + + foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { + $propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']); + $propertyName = (string) preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); + + $this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes); + } + } + } + + protected function processStandardProperty( + DocumentProperties $docProps, + string $propertyName, + string $stringValue + ): void { + switch ($propertyName) { + case 'Title': + $docProps->setTitle($stringValue); + + break; + case 'Subject': + $docProps->setSubject($stringValue); + + break; + case 'Author': + $docProps->setCreator($stringValue); + + break; + case 'Created': + $docProps->setCreated($stringValue); + + break; + case 'LastAuthor': + $docProps->setLastModifiedBy($stringValue); + + break; + case 'LastSaved': + $docProps->setModified($stringValue); + + break; + case 'Company': + $docProps->setCompany($stringValue); + + break; + case 'Category': + $docProps->setCategory($stringValue); + + break; + case 'Manager': + $docProps->setManager($stringValue); + + break; + case 'HyperlinkBase': + $docProps->setHyperlinkBase($stringValue); + + break; + case 'Keywords': + $docProps->setKeywords($stringValue); + + break; + case 'Description': + $docProps->setDescription($stringValue); + + break; + } + } + + protected function processCustomProperty( + DocumentProperties $docProps, + string $propertyName, + ?SimpleXMLElement $propertyValue, + SimpleXMLElement $propertyAttributes + ): void { + switch ((string) $propertyAttributes) { + case 'boolean': + $propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN; + $propertyValue = (bool) (string) $propertyValue; + + break; + case 'integer': + $propertyType = DocumentProperties::PROPERTY_TYPE_INTEGER; + $propertyValue = (int) $propertyValue; + + break; + case 'float': + $propertyType = DocumentProperties::PROPERTY_TYPE_FLOAT; + $propertyValue = (float) $propertyValue; + + break; + case 'dateTime.tz': + case 'dateTime.iso8601tz': + $propertyType = DocumentProperties::PROPERTY_TYPE_DATE; + $propertyValue = trim((string) $propertyValue); + + break; + default: + $propertyType = DocumentProperties::PROPERTY_TYPE_STRING; + $propertyValue = trim((string) $propertyValue); + + break; + } + + $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType); + } + + /** @param string[] $hex */ + protected function hex2str(array $hex): string + { + return mb_chr((int) hexdec($hex[1]), 'UTF-8'); + } + + private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement + { + return ($simple === null) ? new SimpleXMLElement('') : ($simple->attributes($node) ?? new SimpleXMLElement('')); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style.php new file mode 100644 index 0000000..699d718 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style.php @@ -0,0 +1,115 @@ +children('urn:schemas-microsoft-com:office:spreadsheet'); + $stylesXml = $children->Styles[0]; + if (!isset($stylesXml)) { + return []; + } + + $alignmentStyleParser = new Style\Alignment(); + $borderStyleParser = new Style\Border(); + $fontStyleParser = new Style\Font(); + $fillStyleParser = new Style\Fill(); + $numberFormatStyleParser = new Style\NumberFormat(); + + foreach ($stylesXml as $style) { + /** @var SimpleXMLElement $style */ + $style_ss = self::getAttributes($style, $namespaces['ss']); + $styleID = (string) $style_ss['ID']; + $this->styles[$styleID] = $this->styles['Default'] ?? []; + + $alignment = $border = $font = $fill = $numberFormat = $protection = []; + + foreach ($style as $styleType => $styleDatax) { + $styleData = self::getSxml($styleDatax); + $styleAttributes = $styleData->attributes($namespaces['ss']); + + switch ($styleType) { + case 'Alignment': + if ($styleAttributes) { + $alignment = $alignmentStyleParser->parseStyle($styleAttributes); + } + + break; + case 'Borders': + $border = $borderStyleParser->parseStyle($styleData, $namespaces); + + break; + case 'Font': + if ($styleAttributes) { + $font = $fontStyleParser->parseStyle($styleAttributes); + } + + break; + case 'Interior': + if ($styleAttributes) { + $fill = $fillStyleParser->parseStyle($styleAttributes); + } + + break; + case 'NumberFormat': + if ($styleAttributes) { + $numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes); + } + + break; + case 'Protection': + $locked = $hidden = null; + $styleAttributesP = array_key_exists('x', $namespaces) ? $styleData->attributes($namespaces['x']) : []; + if (isset($styleAttributes['Protected'])) { + $locked = ((bool) (string) $styleAttributes['Protected']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED; + } + if (isset($styleAttributesP['HideFormula'])) { + $hidden = ((bool) (string) $styleAttributesP['HideFormula']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED; + } + if ($locked !== null || $hidden !== null) { + $protection['protection'] = []; + if ($locked !== null) { + $protection['protection']['locked'] = $locked; + } + if ($hidden !== null) { + $protection['protection']['hidden'] = $hidden; + } + } + + break; + } + } + + $this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat, $protection); + } + + return $this->styles; + } + + private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement + { + return ($simple === null) ? new SimpleXMLElement('') : ($simple->attributes($node) ?? new SimpleXMLElement('')); + } + + private static function getSxml(?SimpleXMLElement $simple): SimpleXMLElement + { + return ($simple !== null) ? $simple : new SimpleXMLElement(''); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php new file mode 100644 index 0000000..b4afcde --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php @@ -0,0 +1,71 @@ + $styleAttributeValue) { + $styleAttributeValue = (string) $styleAttributeValue; + switch ($styleAttributeKey) { + case 'Vertical': + if (self::identifyFixedStyleValue(self::VERTICAL_ALIGNMENT_STYLES, $styleAttributeValue)) { + $style['alignment']['vertical'] = $styleAttributeValue; + } + + break; + case 'Horizontal': + if (self::identifyFixedStyleValue(self::HORIZONTAL_ALIGNMENT_STYLES, $styleAttributeValue)) { + $style['alignment']['horizontal'] = $styleAttributeValue; + } + + break; + case 'WrapText': + $style['alignment']['wrapText'] = true; + + break; + case 'Rotate': + $style['alignment']['textRotation'] = $styleAttributeValue; + + break; + case 'Indent': + $style['alignment']['indent'] = $styleAttributeValue; + + break; + case 'ReadingOrder': + if ($styleAttributeValue === 'RightToLeft') { + $style['alignment']['readOrder'] = AlignmentStyles::READORDER_RTL; + } elseif ($styleAttributeValue === 'LeftToRight') { + $style['alignment']['readOrder'] = AlignmentStyles::READORDER_LTR; + } + + break; + } + } + + return $style; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Border.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Border.php new file mode 100644 index 0000000..8a5407d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Border.php @@ -0,0 +1,116 @@ + [ + 'continuous' => BorderStyle::BORDER_HAIR, + 'dash' => BorderStyle::BORDER_DASHED, + 'dashdot' => BorderStyle::BORDER_DASHDOT, + 'dashdotdot' => BorderStyle::BORDER_DASHDOTDOT, + 'dot' => BorderStyle::BORDER_DOTTED, + 'double' => BorderStyle::BORDER_DOUBLE, + '0continuous' => BorderStyle::BORDER_HAIR, + '0dash' => BorderStyle::BORDER_DASHED, + '0dashdot' => BorderStyle::BORDER_DASHDOT, + '0dashdotdot' => BorderStyle::BORDER_DASHDOTDOT, + '0dot' => BorderStyle::BORDER_DOTTED, + '0double' => BorderStyle::BORDER_DOUBLE, + '1continuous' => BorderStyle::BORDER_THIN, + '1dash' => BorderStyle::BORDER_DASHED, + '1dashdot' => BorderStyle::BORDER_DASHDOT, + '1dashdotdot' => BorderStyle::BORDER_DASHDOTDOT, + '1dot' => BorderStyle::BORDER_DOTTED, + '1double' => BorderStyle::BORDER_DOUBLE, + '2continuous' => BorderStyle::BORDER_MEDIUM, + '2dash' => BorderStyle::BORDER_MEDIUMDASHED, + '2dashdot' => BorderStyle::BORDER_MEDIUMDASHDOT, + '2dashdotdot' => BorderStyle::BORDER_MEDIUMDASHDOTDOT, + '2dot' => BorderStyle::BORDER_DOTTED, + '2double' => BorderStyle::BORDER_DOUBLE, + '3continuous' => BorderStyle::BORDER_THICK, + '3dash' => BorderStyle::BORDER_MEDIUMDASHED, + '3dashdot' => BorderStyle::BORDER_MEDIUMDASHDOT, + '3dashdotdot' => BorderStyle::BORDER_MEDIUMDASHDOTDOT, + '3dot' => BorderStyle::BORDER_DOTTED, + '3double' => BorderStyle::BORDER_DOUBLE, + ], + ]; + + /** + * @param string[] $namespaces + * + * @return mixed[] + */ + public function parseStyle(SimpleXMLElement $styleData, array $namespaces): array + { + $style = []; + + $diagonalDirection = ''; + $borderPosition = ''; + foreach ($styleData->Border as $borderStyle) { + $borderAttributes = self::getAttributes($borderStyle, $namespaces['ss']); + $thisBorder = []; + $styleType = (string) $borderAttributes->Weight; + $styleType .= strtolower((string) $borderAttributes->LineStyle); + $thisBorder['borderStyle'] = self::BORDER_MAPPINGS['borderStyle'][$styleType] ?? BorderStyle::BORDER_NONE; + + foreach ($borderAttributes as $borderStyleKey => $borderStyleValuex) { + $borderStyleValue = (string) $borderStyleValuex; + switch ($borderStyleKey) { + case 'Position': + /** @var string $diagonalDirection */ + [$borderPosition, $diagonalDirection] + = $this->parsePosition($borderStyleValue, $diagonalDirection); + + break; + case 'Color': + $borderColour = substr($borderStyleValue, 1); + $thisBorder['color']['rgb'] = $borderColour; + + break; + } + } + + /** @var int|string $borderPosition */ + if ($borderPosition) { + $style['borders'][$borderPosition] = $thisBorder; + } elseif ($diagonalDirection) { + $style['borders']['diagonalDirection'] = $diagonalDirection; + $style['borders']['diagonal'] = $thisBorder; + } + } + + return $style; + } + + /** @return mixed[] */ + protected function parsePosition(string $borderStyleValue, string $diagonalDirection): array + { + // TODO diagonalDirection seems to return int not string + $borderStyleValue = strtolower($borderStyleValue); + + if (in_array($borderStyleValue, self::BORDER_POSITIONS)) { + $borderPosition = $borderStyleValue; + } elseif ($borderStyleValue === 'diagonalleft') { + $diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_DOWN; + } elseif ($borderStyleValue === 'diagonalright') { + $diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_UP; + } + + return [$borderPosition ?? null, $diagonalDirection]; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php new file mode 100644 index 0000000..8dc7232 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php @@ -0,0 +1,61 @@ + [ + 'solid' => FillStyles::FILL_SOLID, + 'gray75' => FillStyles::FILL_PATTERN_DARKGRAY, + 'gray50' => FillStyles::FILL_PATTERN_MEDIUMGRAY, + 'gray25' => FillStyles::FILL_PATTERN_LIGHTGRAY, + 'gray125' => FillStyles::FILL_PATTERN_GRAY125, + 'gray0625' => FillStyles::FILL_PATTERN_GRAY0625, + 'horzstripe' => FillStyles::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe + 'vertstripe' => FillStyles::FILL_PATTERN_DARKVERTICAL, // vertical stripe + 'reversediagstripe' => FillStyles::FILL_PATTERN_DARKUP, // reverse diagonal stripe + 'diagstripe' => FillStyles::FILL_PATTERN_DARKDOWN, // diagonal stripe + 'diagcross' => FillStyles::FILL_PATTERN_DARKGRID, // diagoanl crosshatch + 'thickdiagcross' => FillStyles::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch + 'thinhorzstripe' => FillStyles::FILL_PATTERN_LIGHTHORIZONTAL, + 'thinvertstripe' => FillStyles::FILL_PATTERN_LIGHTVERTICAL, + 'thinreversediagstripe' => FillStyles::FILL_PATTERN_LIGHTUP, + 'thindiagstripe' => FillStyles::FILL_PATTERN_LIGHTDOWN, + 'thinhorzcross' => FillStyles::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch + 'thindiagcross' => FillStyles::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch + ], + ]; + + /** @return mixed[] */ + public function parseStyle(SimpleXMLElement $styleAttributes): array + { + $style = []; + + foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValuex) { + $styleAttributeValue = (string) $styleAttributeValuex; + switch ($styleAttributeKey) { + case 'Color': + $style['fill']['endColor']['rgb'] = substr($styleAttributeValue, 1); + $style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'PatternColor': + $style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'Pattern': + $lcStyleAttributeValue = strtolower((string) $styleAttributeValue); + $style['fill']['fillType'] + = self::FILL_MAPPINGS['fillType'][$lcStyleAttributeValue] ?? FillStyles::FILL_NONE; + + break; + } + } + + return $style; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Font.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Font.php new file mode 100644 index 0000000..4a5243a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/Font.php @@ -0,0 +1,91 @@ + $styleAttributeValue) { + $styleAttributeValue = (string) $styleAttributeValue; + switch ($styleAttributeKey) { + case 'FontName': + $style['font']['name'] = $styleAttributeValue; + + break; + case 'Size': + $style['font']['size'] = $styleAttributeValue; + + break; + case 'Color': + /** @var string[][][] $style */ + $style['font']['color']['rgb'] = substr($styleAttributeValue, 1); + + break; + case 'Bold': + $style['font']['bold'] = $styleAttributeValue === '1'; + + break; + case 'Italic': + $style['font']['italic'] = $styleAttributeValue === '1'; + + break; + case 'Underline': + $style = $this->parseUnderline($style, $styleAttributeValue); + + break; + case 'VerticalAlign': + $style = $this->parseVerticalAlign($style, $styleAttributeValue); + + break; + } + } + + return $style; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php new file mode 100644 index 0000000..1ca825a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php @@ -0,0 +1,34 @@ + $styleAttributeValue) { + $styleAttributeValue = str_replace($fromFormats, $toFormats, (string) $styleAttributeValue); + + switch ($styleAttributeValue) { + case 'Short Date': + $styleAttributeValue = 'dd/mm/yyyy'; + + break; + } + + if ($styleAttributeValue > '') { + $style['numberFormat']['formatCode'] = $styleAttributeValue; + } + } + + return $style; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/StyleBase.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/StyleBase.php new file mode 100644 index 0000000..4217cae --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Reader/Xml/Style/StyleBase.php @@ -0,0 +1,31 @@ +') : ($simple->attributes($node) ?? new SimpleXMLElement('')); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/ReferenceHelper.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/ReferenceHelper.php new file mode 100644 index 0000000..65cb5e6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/ReferenceHelper.php @@ -0,0 +1,1387 @@ +getBreaks(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aBreaks, [self::class, 'cellReverseSort']) + : uksort($aBreaks, [self::class, 'cellSort']); + + foreach ($aBreaks as $cellAddress => $value) { + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { + // If we're deleting, then clear any defined breaks that are within the range + // of rows/columns that we're deleting + $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE); + } else { + // Otherwise update any affected breaks by inserting a new break at the appropriate point + // and removing the old affected break + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { + $worksheet->setBreak($newReference, $value) + ->setBreak($cellAddress, Worksheet::BREAK_NONE); + } + } + } + } + + /** + * Update cell comments when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + */ + protected function adjustComments(Worksheet $worksheet): void + { + $aComments = $worksheet->getComments(); + $aNewComments = []; // the new array of all comments + + foreach ($aComments as $cellAddress => &$value) { + // Any comments inside a deleted range will be ignored + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) { + // Otherwise build a new array of comments indexed by the adjusted cell reference + $newReference = $this->updateCellReference($cellAddress); + $aNewComments[$newReference] = $value; + } + } + // Replace the comments array with the new set of comments + $worksheet->setComments($aNewComments); + } + + /** + * Update hyperlinks when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void + { + $aHyperlinkCollection = $worksheet->getHyperlinkCollection(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort']) + : uksort($aHyperlinkCollection, [self::class, 'cellSort']); + + foreach ($aHyperlinkCollection as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { + $worksheet->setHyperlink($cellAddress, null); + } elseif ($cellAddress !== $newReference) { + $worksheet->setHyperlink($cellAddress, null); + if ($newReference) { + $worksheet->setHyperlink($newReference, $value); + } + } + } + } + + /** + * Update conditional formatting styles when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void + { + $aStyles = $worksheet->getConditionalStylesCollection(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aStyles, [self::class, 'cellReverseSort']) + : uksort($aStyles, [self::class, 'cellSort']); + + foreach ($aStyles as $cellAddress => $cfRules) { + $worksheet->removeConditionalStyles($cellAddress); + $newReference = $this->updateCellReference($cellAddress); + + foreach ($cfRules as &$cfRule) { + /** @var Conditional $cfRule */ + $conditions = $cfRule->getConditions(); + foreach ($conditions as &$condition) { + if (is_string($condition)) { + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + $condition = $this->updateFormulaReferences( + $condition, + $cellReferenceHelper->beforeCellAddress(), + $numberOfColumns, + $numberOfRows, + $worksheet->getTitle(), + true + ); + } + } + $cfRule->setConditions($conditions); + } + $worksheet->setConditionalStyles($newReference, $cfRules); + } + } + + /** + * Update data validations when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows, string $beforeCellAddress): void + { + $aDataValidationCollection = $worksheet->getDataValidationCollection(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) + : uksort($aDataValidationCollection, [self::class, 'cellSort']); + + foreach ($aDataValidationCollection as $cellAddress => $dataValidation) { + $formula = $dataValidation->getFormula1(); + if ($formula !== '') { + $dataValidation->setFormula1( + $this->updateFormulaReferences( + $formula, + $beforeCellAddress, + $numberOfColumns, + $numberOfRows, + $worksheet->getTitle(), + true + ) + ); + } + $formula = $dataValidation->getFormula2(); + if ($formula !== '') { + $dataValidation->setFormula2( + $this->updateFormulaReferences( + $formula, + $beforeCellAddress, + $numberOfColumns, + $numberOfRows, + $worksheet->getTitle(), + true + ) + ); + } + $addressParts = explode(' ', $cellAddress); + $newReference = ''; + $separator = ''; + foreach ($addressParts as $addressPart) { + $newReference .= $separator . $this->updateCellReference($addressPart); + $separator = ' '; + } + if ($cellAddress !== $newReference) { + $worksheet->setDataValidation($newReference, $dataValidation); + $worksheet->setDataValidation($cellAddress, null); + if ($newReference) { + $worksheet->setDataValidation($newReference, $dataValidation); + } + } + } + } + + /** + * Update merged cells when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + */ + protected function adjustMergeCells(Worksheet $worksheet): void + { + $aMergeCells = $worksheet->getMergeCells(); + $aNewMergeCells = []; // the new array of all merge cells + foreach ($aMergeCells as $cellAddress => &$value) { + $newReference = $this->updateCellReference($cellAddress); + if ($newReference) { + $aNewMergeCells[$newReference] = $newReference; + } + } + $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array + } + + /** + * Update protected cells when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void + { + $aProtectedCells = $worksheet->getProtectedCellRanges(); + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + if ($numberOfRows >= 0 && $numberOfColumns >= 0) { + foreach ($aProtectedCells as $key2 => $value) { + $ranges = $value->allRanges(); + $newKey = $separator = ''; + foreach ($ranges as $key => $range) { + $oldKey = $range[0] . (array_key_exists(1, $range) ? (':' . $range[1]) : ''); + $newKey .= $separator . $this->updateCellReference($oldKey); + $separator = ' '; + } + if ($key2 !== $newKey) { + $worksheet->unprotectCells($key2); + $worksheet->protectCells($newKey, $value->getPassword(), true, $value->getName(), $value->getSecurityDescriptor()); + } + } + } else { + foreach ($aProtectedCells as $key2 => $value) { + $range = str_replace([' ', ',', "\0"], ["\0", ' ', ','], $key2); + $extracted = Coordinate::extractAllCellReferencesInRange($range); + $outArray = []; + foreach ($extracted as $cellAddress) { + if (!$cellReferenceHelper->cellAddressInDeleteRange($cellAddress)) { + $outArray[$this->updateCellReference($cellAddress)] = 'x'; + } + } + $outArray2 = Coordinate::mergeRangesInCollection($outArray); + $newKey = implode(' ', array_keys($outArray2)); + if ($key2 !== $newKey) { + $worksheet->unprotectCells($key2); + $worksheet->protectCells($newKey, $value->getPassword(), true, $value->getName(), $value->getSecurityDescriptor()); + } + } + } + } + + /** + * Update column dimensions when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + */ + protected function adjustColumnDimensions(Worksheet $worksheet): void + { + $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true); + if (!empty($aColumnDimensions)) { + foreach ($aColumnDimensions as $objColumnDimension) { + $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1'); + [$newReference] = Coordinate::coordinateFromString($newReference); + if ($objColumnDimension->getColumnIndex() !== $newReference) { + $objColumnDimension->setColumnIndex($newReference); + } + } + + $worksheet->refreshColumnDimensions(); + } + } + + /** + * Update row dimensions when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $beforeRow Number of the row we're inserting/deleting before + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustRowDimensions(Worksheet $worksheet, int $beforeRow, int $numberOfRows): void + { + $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true); + if (!empty($aRowDimensions)) { + foreach ($aRowDimensions as $objRowDimension) { + $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex()); + [, $newReference] = Coordinate::coordinateFromString($newReference); + $newRoweference = (int) $newReference; + if ($objRowDimension->getRowIndex() !== $newRoweference) { + $objRowDimension->setRowIndex($newRoweference); + } + } + + $worksheet->refreshRowDimensions(); + + $copyDimension = $worksheet->getRowDimension($beforeRow - 1); + for ($i = $beforeRow; $i <= $beforeRow - 1 + $numberOfRows; ++$i) { + $newDimension = $worksheet->getRowDimension($i); + $newDimension->setRowHeight($copyDimension->getRowHeight()); + $newDimension->setVisible($copyDimension->getVisible()); + $newDimension->setOutlineLevel($copyDimension->getOutlineLevel()); + $newDimension->setCollapsed($copyDimension->getCollapsed()); + } + } + } + + /** + * Insert a new column or row, updating all possible related data. + * + * @param string $beforeCellAddress Insert before this cell address (e.g. 'A1') + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + * @param Worksheet $worksheet The worksheet that we're editing + */ + public function insertNewBefore( + string $beforeCellAddress, + int $numberOfColumns, + int $numberOfRows, + Worksheet $worksheet + ): void { + $remove = ($numberOfColumns < 0 || $numberOfRows < 0); + + if ( + $this->cellReferenceHelper === null + || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + + // Get coordinate of $beforeCellAddress + [$beforeColumn, $beforeRow, $beforeColumnString] = Coordinate::indexesFromString($beforeCellAddress); + + // Clear cells if we are removing columns or rows + $highestColumn = $worksheet->getHighestColumn(); + $highestDataColumn = $worksheet->getHighestDataColumn(); + $highestRow = $worksheet->getHighestRow(); + $highestDataRow = $worksheet->getHighestDataRow(); + + // 1. Clear column strips if we are removing columns + if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) { + $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet); + } + + // 2. Clear row strips if we are removing rows + if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) { + $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet); + } + + // Find missing coordinates. This is important when inserting or deleting column before the last column + $startRow = $startCol = 1; + $startColString = 'A'; + if ($numberOfRows === 0) { + $startCol = $beforeColumn; + $startColString = $beforeColumnString; + } elseif ($numberOfColumns === 0) { + $startRow = $beforeRow; + } + $highColumn = Coordinate::columnIndexFromString($highestDataColumn); + for ($row = $startRow; $row <= $highestDataRow; ++$row) { + for ($col = $startCol, $colString = $startColString; $col <= $highColumn; ++$col, StringHelper::stringIncrement($colString)) { + $worksheet->getCell("$colString$row"); // create cell if it doesn't exist + } + } + + $allCoordinates = $worksheet->getCoordinates(); + if ($remove) { + // It's faster to reverse and pop than to use unshift, especially with large cell collections + $allCoordinates = array_reverse($allCoordinates); + } + + // Loop through cells, bottom-up, and change cell coordinate + while ($coordinate = array_pop($allCoordinates)) { + $cell = $worksheet->getCell($coordinate); + $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); + + // Don't update cells that are being removed + if ($numberOfColumns < 0 && $cellIndex >= $beforeColumn + $numberOfColumns && $cellIndex < $beforeColumn) { + continue; + } + + // New coordinate + $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $numberOfColumns) . ($cell->getRow() + $numberOfRows); + + // Should the cell be updated? Move value and cellXf index from one cell to another. + if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) { + // Update cell styles + $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); + + // Insert this cell at its new location + if ($cell->getDataType() === DataType::TYPE_FORMULA) { + // Formula should be adjusted + $worksheet->getCell($newCoordinate) + ->setValue($this->updateFormulaReferences($cell->getValueString(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true)); + } else { + // Cell value should not be adjusted + $worksheet->getCell($newCoordinate)->setValueExplicit($cell->getValue(), $cell->getDataType()); + } + + // Clear the original cell + $worksheet->getCellCollection()->delete($coordinate); + } else { + /* We don't need to update styles for rows/columns before our insertion position, + but we do still need to adjust any formulae in those cells */ + if ($cell->getDataType() === DataType::TYPE_FORMULA) { + // Formula should be adjusted + $cell->setValue($this->updateFormulaReferences($cell->getValueString(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true)); + } + } + } + + // Duplicate styles for the newly inserted cells + $highestColumn = $worksheet->getHighestColumn(); + $highestRow = $worksheet->getHighestRow(); + + if ($numberOfColumns > 0 && $beforeColumn > 1) { + $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns); + } + + if ($numberOfRows > 0 && $beforeRow - 1 > 0) { + $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows); + } + + // Update worksheet: column dimensions + $this->adjustColumnDimensions($worksheet); + + // Update worksheet: row dimensions + $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows); + + // Update worksheet: page breaks + $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows); + + // Update worksheet: comments + $this->adjustComments($worksheet); + + // Update worksheet: hyperlinks + $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows); + + // Update worksheet: conditional formatting styles + $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows); + + // Update worksheet: data validations + $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows, $beforeCellAddress); + + // Update worksheet: merge cells + $this->adjustMergeCells($worksheet); + + // Update worksheet: protected cells + $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows); + + // Update worksheet: autofilter + $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns); + + // Update worksheet: table + $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns); + + // Update worksheet: freeze pane + if ($worksheet->getFreezePane()) { + $splitCell = $worksheet->getFreezePane(); + $topLeftCell = $worksheet->getTopLeftCell() ?? ''; + + $splitCell = $this->updateCellReference($splitCell); + $topLeftCell = $this->updateCellReference($topLeftCell); + + $worksheet->freezePane($splitCell, $topLeftCell); + } + + $this->updatePrintAreas($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + + // Update worksheet: drawings + $aDrawings = $worksheet->getDrawingCollection(); + foreach ($aDrawings as $objDrawing) { + $newReference = $this->updateCellReference($objDrawing->getCoordinates()); + if ($objDrawing->getCoordinates() != $newReference) { + $objDrawing->setCoordinates($newReference); + } + if ($objDrawing->getCoordinates2() !== '') { + $newReference = $this->updateCellReference($objDrawing->getCoordinates2()); + if ($objDrawing->getCoordinates2() != $newReference) { + $objDrawing->setCoordinates2($newReference); + } + } + } + + // Update workbook: define names + if (count($worksheet->getParentOrThrow()->getDefinedNames()) > 0) { + $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } + + // Garbage collect + $worksheet->garbageCollect(); + } + + private function updatePrintAreas(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + $pageSetup = $worksheet->getPageSetup(); + if (!$pageSetup->isPrintAreaSet()) { + return; + } + $printAreas = explode(',', $pageSetup->getPrintArea()); + $newPrintAreas = []; + foreach ($printAreas as $printArea) { + $result = $this->updatePrintArea($printArea, $beforeCellAddress, $numberOfColumns, $numberOfRows); + if ($result !== '') { + $newPrintAreas[] = $result; + } + } + $result = implode(',', $newPrintAreas); + if ($result === '') { + $pageSetup->clearPrintArea(); + } else { + $pageSetup->setPrintArea($result); + } + } + + private function updatePrintArea(string $printArea, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): string + { + $coordinates = Coordinate::indexesFromString($beforeCellAddress); + if (preg_match('/^([A-Z]{1,3})(\d{1,7}):([A-Z]{1,3})(\d{1,7})$/i', $printArea, $matches) === 1) { + $firstRow = (int) $matches[2]; + $lastRow = (int) $matches[4]; + $firstColumnString = $matches[1]; + $lastColumnString = $matches[3]; + if ($numberOfRows < 0) { + $affectedRow = $coordinates[1] + $numberOfRows - 1; + $lastAffectedRow = $coordinates[1] - 1; + if ($affectedRow >= $firstRow && $affectedRow <= $lastRow) { + $newLastRow = max($affectedRow, $lastRow + $numberOfRows); + if ($newLastRow >= $firstRow) { + return $matches[1] . $matches[2] . ':' . $matches[3] . $newLastRow; + } + + return ''; + } + if ($lastAffectedRow >= $firstRow && $affectedRow <= $lastRow) { + $newFirstRow = $affectedRow + 1; + $newLastRow = $lastRow + $numberOfRows; + if ($newFirstRow >= 1 && $newLastRow >= $newFirstRow) { + return $matches[1] . $newFirstRow . ':' . $matches[3] . $newLastRow; + } + + return ''; + } + } + if ($numberOfColumns < 0) { + $firstColumnInt = Coordinate::columnIndexFromString($firstColumnString); + $lastColumnInt = Coordinate::columnIndexFromString($lastColumnString); + $affectedColumn = $coordinates[0] + $numberOfColumns - 1; + $lastAffectedColumn = $coordinates[0] - 1; + if ($affectedColumn >= $firstColumnInt && $affectedColumn <= $lastColumnInt) { + $newLastColumnInt = max($affectedColumn, $lastColumnInt + $numberOfColumns); + if ($newLastColumnInt >= $firstColumnInt) { + $newLastColumnString = Coordinate::stringFromColumnIndex($newLastColumnInt); + + return $matches[1] . $matches[2] . ':' . $newLastColumnString . $matches[4]; + } + + return ''; + } + if ($affectedColumn < $firstColumnInt && $lastAffectedColumn > $lastColumnInt) { + return ''; + } + if ($lastAffectedColumn >= $firstColumnInt && $lastAffectedColumn <= $lastColumnInt) { + $newFirstColumn = $affectedColumn + 1; + $newLastColumn = $lastColumnInt + $numberOfColumns; + if ($newFirstColumn >= 1 && $newLastColumn >= $newFirstColumn) { + $firstString = Coordinate::stringFromColumnIndex($newFirstColumn); + $lastString = Coordinate::stringFromColumnIndex($newLastColumn); + + return $firstString . $matches[2] . ':' . $lastString . $matches[4]; + } + + return ''; + } + } + } + + return $this->updateCellReference($printArea); + } + + private static function matchSheetName(?string $match, string $worksheetName): bool + { + return $match === null || $match === '' || $match === "'\u{fffc}'" || $match === "'\u{fffb}'" || strcasecmp(trim($match, "'"), $worksheetName) === 0; + } + + private static function sheetnameBeforeCells(string $match, string $worksheetName, string $cells): string + { + $toString = ($match > '') ? "$match!" : ''; + + return str_replace(["\u{fffc}", "'\u{fffb}'"], $worksheetName, $toString) . $cells; + } + + /** + * Update references within formulas. + * + * @param string $formula Formula to update + * @param string $beforeCellAddress Insert before this one + * @param int $numberOfColumns Number of columns to insert + * @param int $numberOfRows Number of rows to insert + * @param string $worksheetName Worksheet name/title + * + * @return string Updated formula + */ + public function updateFormulaReferences( + string $formula = '', + string $beforeCellAddress = 'A1', + int $numberOfColumns = 0, + int $numberOfRows = 0, + string $worksheetName = '', + bool $includeAbsoluteReferences = false, + bool $onlyAbsoluteReferences = false + ): string { + $callback = fn (array $matches): string => (strcasecmp(trim($matches[2], "'"), $worksheetName) === 0) ? (($matches[2][0] === "'") ? "'\u{fffc}'!" : "'\u{fffb}'!") : "'\u{fffd}'!"; + if ( + $this->cellReferenceHelper === null + || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + + // Update cell references in the formula + $formulaBlocks = explode('"', $formula); + $i = false; + foreach ($formulaBlocks as &$formulaBlock) { + // Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode) + $i = $i === false; + if ($i) { + $adjustCount = 0; + $newCellTokens = $cellTokens = []; + // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) + $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' '; + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER); + if ($matchCount > 0) { + foreach ($matches as $match) { + $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}"); + $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 2); + $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 2); + + if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { + if (self::matchSheetName($match[2], $worksheetName)) { + $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4"); + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = 100000; + $row = 10000000 + (int) trim($match[3], '$'); + $cellIndex = "{$column}{$row}"; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + foreach ($matches as $match) { + $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}"); + $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 0, -2); + $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 0, -2); + + if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { + if (self::matchSheetName($match[2], $worksheetName)) { + $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4"); + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000; + $row = 10000000; + $cellIndex = "{$column}{$row}"; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + foreach ($matches as $match) { + $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}"); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true); + $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false); + + if ($match[3] . $match[4] !== $modified3 . $modified4) { + if (self::matchSheetName($match[2], $worksheetName)) { + $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4"); + [$column, $row] = Coordinate::coordinateFromString($match[3]); + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; + $row = (int) trim($row, '$') + 10000000; + $cellIndex = "{$column}{$row}"; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + foreach ($matches as $match) { + $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}"); + + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, null); + if ($match[3] !== $modified3) { + if (self::matchSheetName($match[2], $worksheetName)) { + $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3"); + [$column, $row] = Coordinate::coordinateFromString($match[3]); + $columnAdditionalIndex = $column[0] === '$' ? 1 : 0; + $rowAdditionalIndex = $row[0] === '$' ? 1 : 0; + // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more + $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; + $row = (int) trim($row, '$') + 10000000; + $cellIndex = $row . $rowAdditionalIndex . $column . $columnAdditionalIndex; + + $newCellTokens[$cellIndex] = preg_quote($toString, '/'); + $cellTokens[$cellIndex] = '/(? 0) { + if ($numberOfColumns > 0 || $numberOfRows > 0) { + krsort($cellTokens); + krsort($newCellTokens); + } else { + ksort($cellTokens); + ksort($newCellTokens); + } // Update cell references in the formula + $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock)); + } + } + } + unset($formulaBlock); + + // Then rebuild the formula string + return implode('"', $formulaBlocks); + } + + /** + * Update all cell references within a formula, irrespective of worksheet. + */ + public function updateFormulaReferencesAnyWorksheet(string $formula = '', int $numberOfColumns = 0, int $numberOfRows = 0): string + { + $formula = $this->updateCellReferencesAllWorksheets($formula, $numberOfColumns, $numberOfRows); + + if ($numberOfColumns !== 0) { + $formula = $this->updateColumnRangesAllWorksheets($formula, $numberOfColumns); + } + + if ($numberOfRows !== 0) { + $formula = $this->updateRowRangesAllWorksheets($formula, $numberOfRows); + } + + return $formula; + } + + private function updateCellReferencesAllWorksheets(string $formula, int $numberOfColumns, int $numberOfRows): string + { + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', + $formula, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $columnLengths = array_map('strlen', array_column($splitRanges[6], 0)); + $rowLengths = array_map('strlen', array_column($splitRanges[7], 0)); + $columnOffsets = array_column($splitRanges[6], 1); + $rowOffsets = array_column($splitRanges[7], 1); + + $columns = $splitRanges[6]; + $rows = $splitRanges[7]; + + while ($splitCount > 0) { + --$splitCount; + $columnLength = $columnLengths[$splitCount]; + $rowLength = $rowLengths[$splitCount]; + $columnOffset = $columnOffsets[$splitCount]; + $rowOffset = $rowOffsets[$splitCount]; + $column = $columns[$splitCount][0]; + $row = $rows[$splitCount][0]; + + if ($column[0] !== '$') { + $column = ((Coordinate::columnIndexFromString($column) + $numberOfColumns) % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT; + $column = Coordinate::stringFromColumnIndex($column); + $rowOffset -= ($columnLength - strlen($column)); + $formula = substr($formula, 0, $columnOffset) . $column . substr($formula, $columnOffset + $columnLength); + } + if (!empty($row) && $row[0] !== '$') { + $row = (((int) $row + $numberOfRows) % AddressRange::MAX_ROW) ?: AddressRange::MAX_ROW; + $formula = substr($formula, 0, $rowOffset) . $row . substr($formula, $rowOffset + $rowLength); + } + } + + return $formula; + } + + private function updateColumnRangesAllWorksheets(string $formula, int $numberOfColumns): string + { + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '/mui', + $formula, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $fromColumnLengths = array_map('strlen', array_column($splitRanges[1], 0)); + $fromColumnOffsets = array_column($splitRanges[1], 1); + $toColumnLengths = array_map('strlen', array_column($splitRanges[2], 0)); + $toColumnOffsets = array_column($splitRanges[2], 1); + + $fromColumns = $splitRanges[1]; + $toColumns = $splitRanges[2]; + + while ($splitCount > 0) { + --$splitCount; + $fromColumnLength = $fromColumnLengths[$splitCount]; + $toColumnLength = $toColumnLengths[$splitCount]; + $fromColumnOffset = $fromColumnOffsets[$splitCount]; + $toColumnOffset = $toColumnOffsets[$splitCount]; + $fromColumn = $fromColumns[$splitCount][0]; + $toColumn = $toColumns[$splitCount][0]; + + if (!empty($fromColumn) && $fromColumn[0] !== '$') { + $fromColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($fromColumn) + $numberOfColumns); + $formula = substr($formula, 0, $fromColumnOffset) . $fromColumn . substr($formula, $fromColumnOffset + $fromColumnLength); + } + if (!empty($toColumn) && $toColumn[0] !== '$') { + $toColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($toColumn) + $numberOfColumns); + $formula = substr($formula, 0, $toColumnOffset) . $toColumn . substr($formula, $toColumnOffset + $toColumnLength); + } + } + + return $formula; + } + + private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows): string + { + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '/mui', + $formula, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $fromRowLengths = array_map('strlen', array_column($splitRanges[1], 0)); + $fromRowOffsets = array_column($splitRanges[1], 1); + $toRowLengths = array_map('strlen', array_column($splitRanges[2], 0)); + $toRowOffsets = array_column($splitRanges[2], 1); + + $fromRows = $splitRanges[1]; + $toRows = $splitRanges[2]; + + while ($splitCount > 0) { + --$splitCount; + $fromRowLength = $fromRowLengths[$splitCount]; + $toRowLength = $toRowLengths[$splitCount]; + $fromRowOffset = $fromRowOffsets[$splitCount]; + $toRowOffset = $toRowOffsets[$splitCount]; + $fromRow = $fromRows[$splitCount][0]; + $toRow = $toRows[$splitCount][0]; + + if (!empty($fromRow) && $fromRow[0] !== '$') { + $fromRow = (int) $fromRow + $numberOfRows; + $formula = substr($formula, 0, $fromRowOffset) . $fromRow . substr($formula, $fromRowOffset + $fromRowLength); + } + if (!empty($toRow) && $toRow[0] !== '$') { + $toRow = (int) $toRow + $numberOfRows; + $formula = substr($formula, 0, $toRowOffset) . $toRow . substr($formula, $toRowOffset + $toRowLength); + } + } + + return $formula; + } + + /** + * Update cell reference. + * + * @param string $cellReference Cell address or range of addresses + * + * @return string Updated cell range + */ + private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null) + { + // Is it in another worksheet? Will not have to update anything. + if (str_contains($cellReference, '!')) { + return $cellReference; + } + // Is it a range or a single cell? + if (!Coordinate::coordinateIsRange($cellReference)) { + // Single cell + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + + return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences, $topLeft); + } + + // Range + return $this->updateCellRange($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences); + } + + /** + * Update named formulae (i.e. containing worksheet references / named ranges). + * + * @param Spreadsheet $spreadsheet Object to update + * @param string $oldName Old name (name to replace) + * @param string $newName New name + */ + public function updateNamedFormulae(Spreadsheet $spreadsheet, string $oldName = '', string $newName = ''): void + { + if ($oldName == '') { + return; + } + + foreach ($spreadsheet->getWorksheetIterator() as $sheet) { + foreach ($sheet->getCoordinates(false) as $coordinate) { + $cell = $sheet->getCell($coordinate); + if ($cell->getDataType() === DataType::TYPE_FORMULA) { + $formula = $cell->getValueString(); + if (str_contains($formula, $oldName)) { + $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); + $formula = str_replace($oldName . '!', $newName . '!', $formula); + $cell->setValueExplicit($formula, DataType::TYPE_FORMULA); + } + } + } + } + } + + private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + foreach ($worksheet->getParentOrThrow()->getDefinedNames() as $definedName) { + if ($definedName->isFormula() === false) { + $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } else { + $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } + } + } + + private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + $cellAddress = $definedName->getValue(); + $asFormula = ($cellAddress[0] === '='); + if ($definedName->getWorksheet() === $worksheet) { + /** + * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF! + * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells. + * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF! + * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace + * them with a #REF! + */ + if ($asFormula === true) { + $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true, true); + $definedName->setValue($formula); + } else { + $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true)); + } + } + } + + private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + if ($definedName->getWorksheet() === $worksheet) { + /** + * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF! + * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells. + * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF! + * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace + * them with a #REF! + */ + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true); + $definedName->setValue($formula); + } + } + + /** + * Update cell range. + * + * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') + * + * @return string Updated cell range + */ + private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string + { + if (!Coordinate::coordinateIsRange($cellRange)) { + throw new Exception('Only cell ranges may be passed to this method.'); + } + + // Update range + $range = Coordinate::splitRange($cellRange); + $ic = count($range); + for ($i = 0; $i < $ic; ++$i) { + $jc = count($range[$i]); + for ($j = 0; $j < $jc; ++$j) { + /** @var CellReferenceHelper */ + $cellReferenceHelper = $this->cellReferenceHelper; + if (ctype_alpha($range[$i][$j])) { + $range[$i][$j] = Coordinate::coordinateFromString( + $cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences, null) + )[0]; + } elseif (ctype_digit($range[$i][$j])) { + $range[$i][$j] = Coordinate::coordinateFromString( + $cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null) + )[1]; + } else { + $range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null); + } + } + } + + // Recreate range string + return Coordinate::buildRange($range); + } + + private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void + { + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns); + $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + + for ($row = 1; $row <= $highestRow - 1; ++$row) { + for ($column = $startColumnId; $column !== $endColumnId; StringHelper::stringIncrement($column)) { + $coordinate = $column . $row; + $this->clearStripCell($worksheet, $coordinate); + } + } + } + + private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void + { + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + StringHelper::stringIncrement($highestColumn); + + for ($column = $startColumnId; $column !== $highestColumn; StringHelper::stringIncrement($column)) { + for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) { + $coordinate = $column . $row; + $this->clearStripCell($worksheet, $coordinate); + } + } + } + + private function clearStripCell(Worksheet $worksheet, string $coordinate): void + { + $worksheet->removeConditionalStyles($coordinate); + $worksheet->setHyperlink($coordinate); + $worksheet->setDataValidation($coordinate); + $worksheet->removeComment($coordinate); + + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } + + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void + { + $autoFilter = $worksheet->getAutoFilter(); + $autoFilterRange = $autoFilter->getRange(); + if (!empty($autoFilterRange)) { + if ($numberOfColumns !== 0) { + $autoFilterColumns = $autoFilter->getColumns(); + if (count($autoFilterColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString((string) $column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter); + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in autofilter range + if ($numberOfColumns > 0) { + $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); + } else { + $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); + } + } + } + } + + $worksheet->setAutoFilter( + $this->updateCellReference($autoFilterRange) + ); + } + } + + /** @param mixed[] $autoFilterColumns */ + private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void + { + // If we're actually deleting any columns that fall within the autofilter range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($autoFilterColumns[$columnName])) { + $autoFilter->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $autoFilter->shiftColumn( + Coordinate::stringFromColumnIndex($endColRef), + Coordinate::stringFromColumnIndex($toColRef) + ); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $autoFilter->shiftColumn($startColID, $toColID); + StringHelper::stringIncrement($toColID); + StringHelper::stringIncrement($startColID); + } while ($startColID !== $endColID); + } + + private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void + { + $tableCollection = $worksheet->getTableCollection(); + + foreach ($tableCollection as $table) { + $tableRange = $table->getRange(); + if (!empty($tableRange)) { + if ($numberOfColumns !== 0) { + $tableColumns = $table->getColumns(); + if (count($tableColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString((string) $column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table); + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in table range + if ($numberOfColumns > 0) { + $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table); + } else { + $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table); + } + } + } + } + + $table->setRange($this->updateCellReference($tableRange)); + } + } + } + + /** @param mixed[] $tableColumns */ + private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void + { + // If we're actually deleting any columns that fall within the table range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($tableColumns[$columnName])) { + $table->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $table->shiftColumn( + Coordinate::stringFromColumnIndex($endColRef), + Coordinate::stringFromColumnIndex($toColRef) + ); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $table->shiftColumn($startColID, $toColID); + StringHelper::stringIncrement($toColID); + StringHelper::stringIncrement($startColID); + } while ($startColID !== $endColID); + } + + private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void + { + $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); + for ($i = $beforeRow; $i <= $highestRow; ++$i) { + // Style + $coordinate = $beforeColumnName . $i; + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { + if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) { + $worksheet->getCell([$j, $i])->setXfIndex($xfIndex); + } + } + } + } + } + + private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void + { + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); + for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { + // Style + $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { + if (!empty($xfIndex) || $worksheet->cellExists([$i, $j])) { + $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + } + } + } + } + } + + /** + * __clone implementation. Cloning should not be allowed in a Singleton! + */ + final public function __clone() + { + throw new Exception('Cloning a Singleton is not allowed!'); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/ITextElement.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/ITextElement.php new file mode 100644 index 0000000..548e68b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/ITextElement.php @@ -0,0 +1,34 @@ +richTextElements = []; + + // Rich-Text string attached to cell? + if ($cell !== null) { + // Add cell text and style + if ($cell->getValueString() !== '') { + $objRun = new Run($cell->getValueString()); + $objRun->setFont(clone $cell->getWorksheet()->getStyle($cell->getCoordinate())->getFont()); + $this->addText($objRun); + } + + // Set parent value + $cell->setValueExplicit($this, DataType::TYPE_STRING); + } + } + + /** + * Add text. + * + * @param ITextElement $text Rich text element + * + * @return $this + */ + public function addText(ITextElement $text): static + { + $this->richTextElements[] = $text; + + return $this; + } + + /** + * Create text. + * + * @param string $text Text + */ + public function createText(string $text): TextElement + { + $objText = new TextElement($text); + $this->addText($objText); + + return $objText; + } + + /** + * Create text run. + * + * @param string $text Text + */ + public function createTextRun(string $text): Run + { + $objText = new Run($text); + $this->addText($objText); + + return $objText; + } + + /** + * Get plain text. + */ + public function getPlainText(): string + { + // Return value + $returnValue = ''; + + // Loop through all ITextElements + foreach ($this->richTextElements as $text) { + $returnValue .= $text->getText(); + } + + return $returnValue; + } + + /** + * Convert to string. + */ + public function __toString(): string + { + return $this->getPlainText(); + } + + /** + * Get Rich Text elements. + * + * @return ITextElement[] + */ + public function getRichTextElements(): array + { + return $this->richTextElements; + } + + /** + * Set Rich Text elements. + * + * @param ITextElement[] $textElements Array of elements + * + * @return $this + */ + public function setRichTextElements(array $textElements): static + { + $this->richTextElements = $textElements; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + $hashElements = ''; + foreach ($this->richTextElements as $element) { + $hashElements .= $element->getHashCode(); + } + + return md5( + $hashElements + . __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + $newValue = is_object($value) ? (clone $value) : $value; + if (is_array($value)) { + $newValue = []; + foreach ($value as $key2 => $value2) { + $newValue[$key2] = is_object($value2) ? (clone $value2) : $value2; + } + } + $this->$key = $newValue; + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/Run.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/Run.php new file mode 100644 index 0000000..6a6ccdd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/Run.php @@ -0,0 +1,71 @@ +font = new Font(); + } + + /** + * Get font. + */ + public function getFont(): ?Font + { + return $this->font; + } + + public function getFontOrThrow(): Font + { + if ($this->font === null) { + throw new SpreadsheetException('unexpected null font'); + } + + return $this->font; + } + + /** + * Set font. + * + * @param ?Font $font Font + * + * @return $this + */ + public function setFont(?Font $font = null): static + { + $this->font = $font; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + return md5( + $this->getText() + . (($this->font === null) ? '' : $this->font->getHashCode()) + . __CLASS__ + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/TextElement.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/TextElement.php new file mode 100644 index 0000000..e509d27 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/RichText/TextElement.php @@ -0,0 +1,69 @@ +text = $text; + } + + /** + * Get text. + * + * @return string Text + */ + public function getText(): string + { + return $this->text; + } + + /** + * Set text. + * + * @param string $text Text + * + * @return $this + */ + public function setText(string $text): self + { + $this->text = $text; + + return $this; + } + + /** + * Get font. For this class, the return value is always null. + */ + public function getFont(): ?Font + { + return null; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + return md5( + $this->text + . __CLASS__ + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Settings.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Settings.php new file mode 100644 index 0000000..b4c49d4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Settings.php @@ -0,0 +1,155 @@ + + */ + private static ?string $chartRenderer = null; + + /** + * The cache implementation to be used for cell collection. + */ + private static ?CacheInterface $cache = null; + + /** + * The HTTP client implementation to be used for network request. + */ + private static ?ClientInterface $httpClient = null; + + private static ?RequestFactoryInterface $requestFactory = null; + + /** + * Set the locale code to use for formula translations and any special formatting. + * + * @param string $locale The locale code to use (e.g. "fr" or "pt_br" or "en_uk") + * + * @return bool Success or failure + */ + public static function setLocale(string $locale): bool + { + return Calculation::getInstance()->setLocale($locale); + } + + public static function getLocale(): string + { + return Calculation::getInstance()->getLocale(); + } + + /** + * Identify to PhpSpreadsheet the external library to use for rendering charts. + * + * @param class-string $rendererClassName Class name of the chart renderer + * eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph + */ + public static function setChartRenderer(string $rendererClassName): void + { + // We want phpstan to validate caller, but still need this test + if (!is_a($rendererClassName, IRenderer::class, true)) { //* @phpstan-ignore-line + throw new Exception('Chart renderer must implement ' . IRenderer::class); + } + + self::$chartRenderer = $rendererClassName; + } + + public static function unsetChartRenderer(): void + { + self::$chartRenderer = null; + } + + /** + * Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use. + * + * @return null|class-string Class name of the chart renderer + * eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph + */ + public static function getChartRenderer(): ?string + { + return self::$chartRenderer; + } + + public static function htmlEntityFlags(): int + { + return ENT_COMPAT; + } + + /** + * Sets the implementation of cache that should be used for cell collection. + */ + public static function setCache(?CacheInterface $cache): void + { + self::$cache = $cache; + } + + /** + * Gets the implementation of cache that is being used for cell collection. + */ + public static function getCache(): CacheInterface + { + if (!self::$cache) { + self::$cache = self::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(); + } + + return self::$cache; + } + + public static function useSimpleCacheVersion3(): bool + { + return (new ReflectionClass(CacheInterface::class))->getMethod('get')->getReturnType() !== null; + } + + /** + * Set the HTTP client implementation to be used for network request. + */ + public static function setHttpClient(ClientInterface $httpClient, RequestFactoryInterface $requestFactory): void + { + self::$httpClient = $httpClient; + self::$requestFactory = $requestFactory; + } + + /** + * Unset the HTTP client configuration. + */ + public static function unsetHttpClient(): void + { + self::$httpClient = null; + self::$requestFactory = null; + } + + /** + * Get the HTTP client implementation to be used for network request. + */ + public static function getHttpClient(): ClientInterface + { + if (!self::$httpClient || !self::$requestFactory) { + throw new Exception('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.'); + } + + return self::$httpClient; + } + + /** + * Get the HTTP request factory. + */ + public static function getRequestFactory(): RequestFactoryInterface + { + if (!self::$httpClient || !self::$requestFactory) { + throw new Exception('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.'); + } + + return self::$requestFactory; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/CodePage.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/CodePage.php new file mode 100644 index 0000000..540e96c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/CodePage.php @@ -0,0 +1,115 @@ +|string> */ + private static array $pageArray = [ + 0 => 'CP1252', // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program + 367 => 'ASCII', // ASCII + 437 => 'CP437', // OEM US + //720 => 'notsupported', // OEM Arabic + 737 => 'CP737', // OEM Greek + 775 => 'CP775', // OEM Baltic + 850 => 'CP850', // OEM Latin I + 852 => 'CP852', // OEM Latin II (Central European) + 855 => 'CP855', // OEM Cyrillic + 857 => 'CP857', // OEM Turkish + 858 => 'CP858', // OEM Multilingual Latin I with Euro + 860 => 'CP860', // OEM Portugese + 861 => 'CP861', // OEM Icelandic + 862 => 'CP862', // OEM Hebrew + 863 => 'CP863', // OEM Canadian (French) + 864 => 'CP864', // OEM Arabic + 865 => 'CP865', // OEM Nordic + 866 => 'CP866', // OEM Cyrillic (Russian) + 869 => 'CP869', // OEM Greek (Modern) + 874 => 'CP874', // ANSI Thai + 932 => 'CP932', // ANSI Japanese Shift-JIS + 936 => 'CP936', // ANSI Chinese Simplified GBK + 949 => 'CP949', // ANSI Korean (Wansung) + 950 => 'CP950', // ANSI Chinese Traditional BIG5 + 1200 => 'UTF-16LE', // UTF-16 (BIFF8) + 1250 => 'CP1250', // ANSI Latin II (Central European) + 1251 => 'CP1251', // ANSI Cyrillic + 1252 => 'CP1252', // ANSI Latin I (BIFF4-BIFF7) + 1253 => 'CP1253', // ANSI Greek + 1254 => 'CP1254', // ANSI Turkish + 1255 => 'CP1255', // ANSI Hebrew + 1256 => 'CP1256', // ANSI Arabic + 1257 => 'CP1257', // ANSI Baltic + 1258 => 'CP1258', // ANSI Vietnamese + 1361 => 'CP1361', // ANSI Korean (Johab) + 10000 => 'MAC', // Apple Roman + 10001 => 'CP932', // Macintosh Japanese + 10002 => 'CP950', // Macintosh Chinese Traditional + 10003 => 'CP1361', // Macintosh Korean + 10004 => 'MACARABIC', // Apple Arabic + 10005 => 'MACHEBREW', // Apple Hebrew + 10006 => 'MACGREEK', // Macintosh Greek + 10007 => 'MACCYRILLIC', // Macintosh Cyrillic + 10008 => 'CP936', // Macintosh - Simplified Chinese (GB 2312) + 10010 => 'MACROMANIA', // Macintosh Romania + 10017 => 'MACUKRAINE', // Macintosh Ukraine + 10021 => 'MACTHAI', // Macintosh Thai + 10029 => ['MACCENTRALEUROPE', 'MAC-CENTRALEUROPE'], // Macintosh Central Europe + 10079 => 'MACICELAND', // Macintosh Icelandic + 10081 => 'MACTURKISH', // Macintosh Turkish + 10082 => 'MACCROATIAN', // Macintosh Croatian + 21010 => 'UTF-16LE', // UTF-16 (BIFF8) This isn't correct, but some Excel writer libraries erroneously use Codepage 21010 for UTF-16LE + 32768 => 'MAC', // Apple Roman + //32769 => 'unsupported', // ANSI Latin I (BIFF2-BIFF3) + 65000 => 'UTF-7', // Unicode (UTF-7) + 65001 => 'UTF-8', // Unicode (UTF-8) + 99999 => ['unsupported'], // Unicode (UTF-8) + ]; + + public static function validate(string $codePage): bool + { + return in_array($codePage, self::$pageArray, true); + } + + /** + * Convert Microsoft Code Page Identifier to Code Page Name which iconv + * and mbstring understands. + * + * @param int $codePage Microsoft Code Page Identifier + * + * @return string Code Page Name + */ + public static function numberToName(int $codePage): string + { + if (array_key_exists($codePage, self::$pageArray)) { + $value = self::$pageArray[$codePage]; + if (is_array($value)) { + foreach ($value as $encoding) { + if (@iconv('UTF-8', $encoding, ' ') !== false) { + self::$pageArray[$codePage] = $encoding; + + return $encoding; + } + } + + throw new PhpSpreadsheetException("Code page $codePage not implemented on this system."); + } else { + return $value; + } + } + if ($codePage == 720 || $codePage == 32769) { + throw new PhpSpreadsheetException("Code page $codePage not supported."); // OEM Arabic + } + + throw new PhpSpreadsheetException('Unknown codepage: ' . $codePage); + } + + /** @return array|string> */ + public static function getEncodings(): array + { + return self::$pageArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Date.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Date.php new file mode 100644 index 0000000..d30f0ed --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Date.php @@ -0,0 +1,565 @@ + 'January', + 'Feb' => 'February', + 'Mar' => 'March', + 'Apr' => 'April', + 'May' => 'May', + 'Jun' => 'June', + 'Jul' => 'July', + 'Aug' => 'August', + 'Sep' => 'September', + 'Oct' => 'October', + 'Nov' => 'November', + 'Dec' => 'December', + ]; + + /** + * @var string[] + */ + public static array $numberSuffixes = [ + 'st', + 'nd', + 'rd', + 'th', + ]; + + /** + * Base calendar year to use for calculations + * Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904). + */ + protected static int $excelCalendar = self::CALENDAR_WINDOWS_1900; + + /** + * Default timezone to use for DateTime objects. + */ + protected static ?DateTimeZone $defaultTimeZone = null; + + /** + * Set the Excel calendar (Windows 1900 or Mac 1904). + * + * @param ?int $baseYear Excel base date (1900 or 1904) + * + * @return bool Success or failure + */ + public static function setExcelCalendar(?int $baseYear): bool + { + if ( + ($baseYear === self::CALENDAR_WINDOWS_1900) + || ($baseYear === self::CALENDAR_MAC_1904) + ) { + self::$excelCalendar = $baseYear; + + return true; + } + + return false; + } + + /** + * Return the Excel calendar (Windows 1900 or Mac 1904). + * + * @return int Excel base date (1900 or 1904) + */ + public static function getExcelCalendar(): int + { + return self::$excelCalendar; + } + + /** + * Set the Default timezone to use for dates. + * + * @param null|DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions + * + * @return bool Success or failure + */ + public static function setDefaultTimezone($timeZone): bool + { + try { + $timeZone = self::validateTimeZone($timeZone); + self::$defaultTimeZone = $timeZone; + $retval = true; + } catch (PhpSpreadsheetException) { + $retval = false; + } + + return $retval; + } + + /** + * Return the Default timezone, or UTC if default not set. + */ + public static function getDefaultTimezone(): DateTimeZone + { + return self::$defaultTimeZone ?? new DateTimeZone('UTC'); + } + + /** + * Return the Default timezone, or local timezone if default is not set. + */ + public static function getDefaultOrLocalTimezone(): DateTimeZone + { + return self::$defaultTimeZone ?? new DateTimeZone(date_default_timezone_get()); + } + + /** + * Return the Default timezone even if null. + */ + public static function getDefaultTimezoneOrNull(): ?DateTimeZone + { + return self::$defaultTimeZone; + } + + /** + * Validate a timezone. + * + * @param null|DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object + * + * @return ?DateTimeZone The timezone as a timezone object + */ + private static function validateTimeZone($timeZone): ?DateTimeZone + { + if ($timeZone instanceof DateTimeZone || $timeZone === null) { + return $timeZone; + } + if (in_array($timeZone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC))) { + return new DateTimeZone($timeZone); + } + + throw new PhpSpreadsheetException('Invalid timezone'); + } + + /** + * @param mixed $value Converts a date/time in ISO-8601 standard format date string to an Excel + * serialized timestamp. + * See https://en.wikipedia.org/wiki/ISO_8601 for details of the ISO-8601 standard format. + */ + public static function convertIsoDate(mixed $value): float|int + { + if (!is_string($value)) { + throw new Exception('Non-string value supplied for Iso Date conversion'); + } + + $date = new DateTime($value); + $dateErrors = DateTime::getLastErrors(); + + if (is_array($dateErrors) && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) { + throw new Exception("Invalid string $value supplied for datatype Date"); + } + + $newValue = self::dateTimeToExcel($date); + + if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) { + $newValue = fmod($newValue, 1.0); + } + + return $newValue; + } + + /** + * Convert a MS serialized datetime value from Excel to a PHP Date/Time object. + * + * @param float|int $excelTimestamp MS Excel serialized date/time value + * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp, + * if you don't want to treat it as a UTC value + * Use the default (UTC) unless you absolutely need a conversion + * + * @return DateTime PHP date/time object + */ + public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null): DateTime + { + $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone); + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { + if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) { + // Unix timestamp base date + $baseDate = new DateTime('1970-01-01', $timeZone); + } else { + // MS Excel calendar base dates + if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { + // Allow adjustment for 1900 Leap Year in MS Excel + $baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone); + } else { + $baseDate = new DateTime('1904-01-01', $timeZone); + } + } + } else { + $baseDate = new DateTime('1899-12-30', $timeZone); + } + + if (is_int($excelTimestamp)) { + if ($excelTimestamp >= 0) { + return $baseDate->modify("+ $excelTimestamp days"); + } + + return $baseDate->modify("$excelTimestamp days"); + } + $days = floor($excelTimestamp); + $partDay = $excelTimestamp - $days; + $hms = 86400 * $partDay; + $microseconds = (int) round(fmod($hms, 1) * 1000000); + $hms = (int) floor($hms); + $hours = intdiv($hms, 3600); + $hms -= $hours * 3600; + $minutes = intdiv($hms, 60); + $seconds = $hms % 60; + + if ($days >= 0) { + $days = '+' . $days; + } + $interval = $days . ' days'; + + return $baseDate->modify($interval) + ->setTime($hours, $minutes, $seconds, $microseconds); + } + + /** + * Convert a MS serialized datetime value from Excel to a unix timestamp. + * The use of Unix timestamps, and therefore this function, is discouraged. + * They are not Y2038-safe on a 32-bit system, and have no timezone info. + * + * @param float|int $excelTimestamp MS Excel serialized date/time value + * @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp, + * if you don't want to treat it as a UTC value + * Use the default (UTC) unless you absolutely need a conversion + * + * @return int Unix timetamp for this date/time + */ + public static function excelToTimestamp($excelTimestamp, $timeZone = null): int + { + $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone); + self::roundMicroseconds($dto); + + return (int) $dto->format('U'); + } + + /** + * Convert a date from PHP to an MS Excel serialized date/time value. + * + * @param mixed $dateValue PHP DateTime object or a string - Unix timestamp is also permitted, but discouraged; + * not Y2038-safe on a 32-bit system, and no timezone info + * + * @return false|float Excel date/time value + * or boolean FALSE on failure + */ + public static function PHPToExcel(mixed $dateValue) + { + if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) { + return self::dateTimeToExcel($dateValue); + } elseif (is_numeric($dateValue)) { + return self::timestampToExcel($dateValue); + } elseif (is_string($dateValue)) { + return self::stringToExcel($dateValue); + } + + return false; + } + + /** + * Convert a PHP DateTime object to an MS Excel serialized date/time value. + * + * @param DateTimeInterface $dateValue PHP DateTime object + * + * @return float MS Excel serialized date/time value + */ + public static function dateTimeToExcel(DateTimeInterface $dateValue): float + { + $seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u')); + + return self::formattedPHPToExcel( + (int) $dateValue->format('Y'), + (int) $dateValue->format('m'), + (int) $dateValue->format('d'), + (int) $dateValue->format('H'), + (int) $dateValue->format('i'), + $seconds + ); + } + + /** + * Convert a Unix timestamp to an MS Excel serialized date/time value. + * The use of Unix timestamps, and therefore this function, is discouraged. + * They are not Y2038-safe on a 32-bit system, and have no timezone info. + * + * @param float|int|string $unixTimestamp Unix Timestamp + * + * @return false|float MS Excel serialized date/time value + */ + public static function timestampToExcel($unixTimestamp): bool|float + { + if (!is_numeric($unixTimestamp)) { + return false; + } + + return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp)); + } + + /** + * formattedPHPToExcel. + * + * @return float Excel date/time value + */ + public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0): float + { + if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { + // + // Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel + // This affects every date following 28th February 1900 + // + $excel1900isLeapYear = true; + if (($year == 1900) && ($month <= 2)) { + $excel1900isLeapYear = false; + } + $myexcelBaseDate = 2415020; + } else { + $myexcelBaseDate = 2416481; + $excel1900isLeapYear = false; + } + + // Julian base date Adjustment + if ($month > 2) { + $month -= 3; + } else { + $month += 9; + --$year; + } + + // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) + $century = (int) substr((string) $year, 0, 2); + $decade = (int) substr((string) $year, 2, 2); + $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear; + + $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; + + return (float) $excelDate + $excelTime; + } + + /** + * Is a given cell a date/time? + */ + public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWithoutTimeOkay = true): bool + { + $result = false; + $worksheet = $cell->getWorksheetOrNull(); + $spreadsheet = ($worksheet === null) ? null : $worksheet->getParent(); + if ($worksheet !== null && $spreadsheet !== null) { + $index = $spreadsheet->getActiveSheetIndex(); + $selected = $worksheet->getSelectedCells(); + + try { + if ($value === null) { + $value = Functions::flattenSingleValue( + $cell->getCalculatedValue() + ); + } + if (is_numeric($value)) { + $result = self::isDateTimeFormat( + $worksheet->getStyle( + $cell->getCoordinate() + )->getNumberFormat(), + $dateWithoutTimeOkay + ); + /** @var float|int $value */ + self::excelToDateTimeObject($value); + } + } catch (Throwable) { + $result = false; + } + $worksheet->setSelectedCells($selected); + $spreadsheet->setActiveSheetIndex($index); + } + + return $result; + } + + /** + * Is a given NumberFormat code a date/time format code? + */ + public static function isDateTimeFormat(NumberFormat $excelFormatCode, bool $dateWithoutTimeOkay = true): bool + { + return self::isDateTimeFormatCode((string) $excelFormatCode->getFormatCode(), $dateWithoutTimeOkay); + } + + private const POSSIBLE_DATETIME_FORMAT_CHARACTERS = 'eymdHs'; + private const POSSIBLE_TIME_FORMAT_CHARACTERS = 'Hs'; // note - no 'm' due to ambiguity + + /** + * Is a given number format code a date/time? + */ + public static function isDateTimeFormatCode(string $excelFormatCode, bool $dateWithoutTimeOkay = true): bool + { + if (strtolower($excelFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) { + // "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check) + return false; + } + if (preg_match('/[0#]E[+-]0/i', $excelFormatCode)) { + // Scientific format + return false; + } + + // Switch on formatcode + $excelFormatCode = (string) NumberFormat::convertSystemFormats($excelFormatCode); + if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) { + return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY); + } + + // Typically number, currency or accounting (or occasionally fraction) formats + if ((str_starts_with($excelFormatCode, '_')) || (str_starts_with($excelFormatCode, '0 '))) { + return false; + } + // Some "special formats" provided in German Excel versions were detected as date time value, + // so filter them out here - "\C\H\-00000" (Switzerland) and "\D-00000" (Germany). + if (str_contains($excelFormatCode, '-00000')) { + return false; + } + $possibleFormatCharacters = $dateWithoutTimeOkay ? self::POSSIBLE_DATETIME_FORMAT_CHARACTERS : self::POSSIBLE_TIME_FORMAT_CHARACTERS; + // Try checking for any of the date formatting characters that don't appear within square braces + if (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $excelFormatCode)) { + // We might also have a format mask containing quoted strings... + // we don't want to test for any of our characters within the quoted blocks + if (str_contains($excelFormatCode, '"')) { + $segMatcher = false; + foreach (explode('"', $excelFormatCode) as $subVal) { + // Only test in alternate array entries (the non-quoted blocks) + $segMatcher = $segMatcher === false; + if ( + $segMatcher + && (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $subVal)) + ) { + return true; + } + } + + return false; + } + + return true; + } + + // No date... + return false; + } + + /** + * Convert a date/time string to Excel time. + * + * @param string $dateValue Examples: '2009-12-31', '2009-12-31 15:59', '2009-12-31 15:59:10' + * + * @return false|float Excel date/time serial value + */ + public static function stringToExcel(string $dateValue): bool|float + { + if (strlen($dateValue) < 2) { + return false; + } + if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2}([.]\d+)?)?)?$/iu', $dateValue)) { + return false; + } + + $dateValueNew = DateTimeExcel\DateValue::fromString($dateValue); + + if (!is_float($dateValueNew)) { + return false; + } + + if (str_contains($dateValue, ':')) { + $timeValue = DateTimeExcel\TimeValue::fromString($dateValue); + if (!is_float($timeValue)) { + return false; + } + $dateValueNew += $timeValue; + } + + return $dateValueNew; + } + + /** + * Converts a month name (either a long or a short name) to a month number. + * + * @param string $monthName Month name or abbreviation + * + * @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name + */ + public static function monthStringToNumber(string $monthName) + { + $monthIndex = 1; + foreach (self::$monthNames as $shortMonthName => $longMonthName) { + if (($monthName === $longMonthName) || ($monthName === $shortMonthName)) { + return $monthIndex; + } + ++$monthIndex; + } + + return $monthName; + } + + /** + * Strips an ordinal from a numeric value. + * + * @param string $day Day number with an ordinal + * + * @return int|string The integer value with any ordinal stripped, or the original string argument if it isn't a valid numeric + */ + public static function dayStringToNumber(string $day) + { + $strippedDayValue = (str_replace(self::$numberSuffixes, '', $day)); + if (is_numeric($strippedDayValue)) { + return (int) $strippedDayValue; + } + + return $day; + } + + public static function dateTimeFromTimestamp(string $date, ?DateTimeZone $timeZone = null): DateTime + { + $dtobj = DateTime::createFromFormat('U', $date) ?: new DateTime(); + $dtobj->setTimeZone($timeZone ?? self::getDefaultOrLocalTimezone()); + + return $dtobj; + } + + public static function formattedDateTimeFromTimestamp(string $date, string $format, ?DateTimeZone $timeZone = null): string + { + $dtobj = self::dateTimeFromTimestamp($date, $timeZone); + + return $dtobj->format($format); + } + + /** + * Round the given DateTime object to seconds. + */ + public static function roundMicroseconds(DateTime $dti): void + { + $microseconds = (int) $dti->format('u'); + $rounded = (int) round($microseconds, -6); + $modify = $rounded - $microseconds; + if ($modify !== 0) { + $dti->modify(($modify > 0 ? '+' : '') . $modify . ' microseconds'); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Drawing.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Drawing.php new file mode 100644 index 0000000..f21cfaa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Drawing.php @@ -0,0 +1,150 @@ +getName(); + $size = $defaultFont->getSize(); + $sizex = ($size !== null && $size == (int) $size) ? ((int) $size) : "$size"; + + if (isset(Font::DEFAULT_COLUMN_WIDTHS[$name][$sizex])) { + // Exact width can be determined + return $pixelValue * Font::DEFAULT_COLUMN_WIDTHS[$name][$sizex]['width'] + / Font::DEFAULT_COLUMN_WIDTHS[$name][$sizex]['px']; + } + + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + return $pixelValue * 11 * Font::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width'] + / Font::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] / $size; + } + + /** + * Convert column width from (intrinsic) Excel units to pixels. + * + * @param float $cellWidth Value in cell dimension + * @param \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Default font of the workbook + * + * @return int Value in pixels + */ + public static function cellDimensionToPixels(float $cellWidth, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont): int + { + // Font name and size + $name = $defaultFont->getName(); + $size = $defaultFont->getSize(); + $sizex = ($size !== null && $size == (int) $size) ? ((int) $size) : "$size"; + + if (isset(Font::DEFAULT_COLUMN_WIDTHS[$name][$sizex])) { + // Exact width can be determined + $colWidth = $cellWidth * Font::DEFAULT_COLUMN_WIDTHS[$name][$sizex]['px'] + / Font::DEFAULT_COLUMN_WIDTHS[$name][$sizex]['width']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $colWidth = $cellWidth * $size * Font::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] + / Font::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width'] / 11; + } + + // Round pixels to closest integer + $colWidth = (int) round($colWidth); + + return $colWidth; + } + + /** + * Convert pixels to points. + * + * @param int $pixelValue Value in pixels + * + * @return float Value in points + */ + public static function pixelsToPoints(int $pixelValue): float + { + return $pixelValue * 0.75; + } + + /** + * Convert points to pixels. + * + * @param float|int $pointValue Value in points + * + * @return int Value in pixels + */ + public static function pointsToPixels($pointValue): int + { + return (int) ceil($pointValue / 0.75); + } + + /** + * Convert degrees to angle. + * + * @param int $degrees Degrees + * + * @return int Angle + */ + public static function degreesToAngle(int $degrees): int + { + return (int) round($degrees * 60000); + } + + /** + * Convert angle to degrees. + * + * @param int|SimpleXMLElement $angle Angle + * + * @return int Degrees + */ + public static function angleToDegrees($angle): int + { + $angle = (int) $angle; + if ($angle != 0) { + return (int) round($angle / 60000); + } + + return 0; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher.php new file mode 100644 index 0000000..00180f6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher.php @@ -0,0 +1,66 @@ +dggContainer; + } + + /** + * Get Drawing Group Container. + */ + public function getDggContainerOrThrow(): Escher\DggContainer + { + return $this->dggContainer ?? throw new SpreadsheetException('dggContainer is unexpectedly null'); + } + + /** + * Set Drawing Group Container. + */ + public function setDggContainer(Escher\DggContainer $dggContainer): Escher\DggContainer + { + return $this->dggContainer = $dggContainer; + } + + /** + * Get Drawing Container. + */ + public function getDgContainer(): ?Escher\DgContainer + { + return $this->dgContainer; + } + + /** + * Get Drawing Container. + */ + public function getDgContainerOrThrow(): Escher\DgContainer + { + return $this->dgContainer ?? throw new SpreadsheetException('dgContainer is unexpectedly null'); + } + + /** + * Set Drawing Container. + */ + public function setDgContainer(Escher\DgContainer $dgContainer): Escher\DgContainer + { + return $this->dgContainer = $dgContainer; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer.php new file mode 100644 index 0000000..0f4b7a8 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer.php @@ -0,0 +1,60 @@ +dgId; + } + + public function setDgId(int $value): void + { + $this->dgId = $value; + } + + public function getLastSpId(): ?int + { + return $this->lastSpId; + } + + public function setLastSpId(int $value): void + { + $this->lastSpId = $value; + } + + public function getSpgrContainer(): ?SpgrContainer + { + return $this->spgrContainer; + } + + public function getSpgrContainerOrThrow(): SpgrContainer + { + if ($this->spgrContainer !== null) { + return $this->spgrContainer; + } + + throw new SpreadsheetException('spgrContainer is unexpectedly null'); + } + + public function setSpgrContainer(SpgrContainer $spgrContainer): SpgrContainer + { + return $this->spgrContainer = $spgrContainer; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php new file mode 100644 index 0000000..0670a2c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php @@ -0,0 +1,76 @@ +parent = $parent; + } + + /** + * Get the parent Shape Group Container if any. + */ + public function getParent(): ?self + { + return $this->parent; + } + + /** + * Add a child. This will be either spgrContainer or spContainer. + * + * @param SpgrContainer|SpgrContainer\SpContainer $child child to be added + */ + public function addChild(mixed $child): void + { + $this->children[] = $child; + $child->setParent($this); + } + + /** + * Get collection of Shape Containers. + * + * @return mixed[] + */ + public function getChildren(): array + { + return $this->children; + } + + /** + * Recursively get all spContainers within this spgrContainer. + * + * @return SpgrContainer\SpContainer[] + */ + public function getAllSpContainers(): array + { + $allSpContainers = []; + + foreach ($this->children as $child) { + if ($child instanceof self) { + $allSpContainers = array_merge($allSpContainers, $child->getAllSpContainers()); + } else { + $allSpContainers[] = $child; + } + } + /** @var SpgrContainer\SpContainer[] $allSpContainers */ + + return $allSpContainers; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php new file mode 100644 index 0000000..45a2738 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php @@ -0,0 +1,304 @@ +parent = $parent; + } + + /** + * Get the parent Shape Group Container. + */ + public function getParent(): SpgrContainer + { + return $this->parent; + } + + /** + * Set whether this is a group shape. + */ + public function setSpgr(bool $value): void + { + $this->spgr = $value; + } + + /** + * Get whether this is a group shape. + */ + public function getSpgr(): bool + { + return $this->spgr; + } + + /** + * Set the shape type. + */ + public function setSpType(int $value): void + { + $this->spType = $value; + } + + /** + * Get the shape type. + */ + public function getSpType(): int + { + return $this->spType; + } + + /** + * Set the shape flag. + */ + public function setSpFlag(int $value): void + { + $this->spFlag = $value; + } + + /** + * Get the shape flag. + */ + public function getSpFlag(): int + { + return $this->spFlag; + } + + /** + * Set the shape index. + */ + public function setSpId(int $value): void + { + $this->spId = $value; + } + + /** + * Get the shape index. + */ + public function getSpId(): int + { + return $this->spId; + } + + /** + * Set an option for the Shape Group Container. + * + * @param int $property The number specifies the option + */ + public function setOPT(int $property, mixed $value): void + { + $this->OPT[$property] = $value; + } + + /** + * Get an option for the Shape Group Container. + * + * @param int $property The number specifies the option + */ + public function getOPT(int $property): mixed + { + if (isset($this->OPT[$property])) { + return $this->OPT[$property]; + } + + return null; + } + + /** + * Get the collection of options. + * + * @return mixed[] + */ + public function getOPTCollection(): array + { + return $this->OPT; + } + + /** + * Set cell coordinates of upper-left corner of shape. + * + * @param string $value eg: 'A1' + */ + public function setStartCoordinates(string $value): void + { + $this->startCoordinates = $value; + } + + /** + * Get cell coordinates of upper-left corner of shape. + */ + public function getStartCoordinates(): string + { + return $this->startCoordinates; + } + + /** + * Set offset in x-direction of upper-left corner of shape measured in 1/1024 of column width. + */ + public function setStartOffsetX(int|float $startOffsetX): void + { + $this->startOffsetX = $startOffsetX; + } + + /** + * Get offset in x-direction of upper-left corner of shape measured in 1/1024 of column width. + */ + public function getStartOffsetX(): int|float + { + return $this->startOffsetX; + } + + /** + * Set offset in y-direction of upper-left corner of shape measured in 1/256 of row height. + */ + public function setStartOffsetY(int|float $startOffsetY): void + { + $this->startOffsetY = $startOffsetY; + } + + /** + * Get offset in y-direction of upper-left corner of shape measured in 1/256 of row height. + */ + public function getStartOffsetY(): int|float + { + return $this->startOffsetY; + } + + /** + * Set cell coordinates of bottom-right corner of shape. + * + * @param string $value eg: 'A1' + */ + public function setEndCoordinates(string $value): void + { + $this->endCoordinates = $value; + } + + /** + * Get cell coordinates of bottom-right corner of shape. + */ + public function getEndCoordinates(): string + { + return $this->endCoordinates; + } + + /** + * Set offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width. + */ + public function setEndOffsetX(int|float $endOffsetX): void + { + $this->endOffsetX = $endOffsetX; + } + + /** + * Get offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width. + */ + public function getEndOffsetX(): int|float + { + return $this->endOffsetX; + } + + /** + * Set offset in y-direction of bottom-right corner of shape measured in 1/256 of row height. + */ + public function setEndOffsetY(int|float $endOffsetY): void + { + $this->endOffsetY = $endOffsetY; + } + + /** + * Get offset in y-direction of bottom-right corner of shape measured in 1/256 of row height. + */ + public function getEndOffsetY(): int|float + { + return $this->endOffsetY; + } + + /** + * Get the nesting level of this spContainer. This is the number of spgrContainers between this spContainer and + * the dgContainer. A value of 1 = immediately within first spgrContainer + * Higher nesting level occurs if and only if spContainer is part of a shape group. + * + * @return int Nesting level + */ + public function getNestingLevel(): int + { + $nestingLevel = 0; + + $parent = $this->getParent(); + while ($parent instanceof SpgrContainer) { + ++$nestingLevel; + $parent = $parent->getParent(); + } + + return $nestingLevel; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer.php new file mode 100644 index 0000000..f7053dd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer.php @@ -0,0 +1,158 @@ +spIdMax; + } + + /** + * Set maximum shape index of all shapes in all drawings (plus one). + */ + public function setSpIdMax(int $value): void + { + $this->spIdMax = $value; + } + + /** + * Get total number of drawings saved. + */ + public function getCDgSaved(): int + { + return $this->cDgSaved; + } + + /** + * Set total number of drawings saved. + */ + public function setCDgSaved(int $value): void + { + $this->cDgSaved = $value; + } + + /** + * Get total number of shapes saved (including group shapes). + */ + public function getCSpSaved(): int + { + return $this->cSpSaved; + } + + /** + * Set total number of shapes saved (including group shapes). + */ + public function setCSpSaved(int $value): void + { + $this->cSpSaved = $value; + } + + /** + * Get BLIP Store Container. + */ + public function getBstoreContainer(): ?DggContainer\BstoreContainer + { + return $this->bstoreContainer; + } + + /** + * Get BLIP Store Container. + */ + public function getBstoreContainerOrThrow(): DggContainer\BstoreContainer + { + return $this->bstoreContainer ?? throw new SpreadsheetException('bstoreContainer is unexpectedly null'); + } + + /** + * Set BLIP Store Container. + */ + public function setBstoreContainer(DggContainer\BstoreContainer $bstoreContainer): void + { + $this->bstoreContainer = $bstoreContainer; + } + + /** + * Set an option for the drawing group. + * + * @param int $property The number specifies the option + */ + public function setOPT(int $property, mixed $value): void + { + $this->OPT[$property] = $value; + } + + /** + * Get an option for the drawing group. + * + * @param int $property The number specifies the option + */ + public function getOPT(int $property): mixed + { + if (isset($this->OPT[$property])) { + return $this->OPT[$property]; + } + + return null; + } + + /** + * Get identifier clusters. + * + * @return mixed[] + */ + public function getIDCLs(): array + { + return $this->IDCLs; + } + + /** + * Set identifier clusters. [ => , ...]. + * + * @param mixed[] $IDCLs + */ + public function setIDCLs(array $IDCLs): void + { + $this->IDCLs = $IDCLs; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php new file mode 100644 index 0000000..2d13b42 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php @@ -0,0 +1,32 @@ +BSECollection[] = $BSE; + $BSE->setParent($this); + } + + /** + * Get the collection of BLIP Store Entries. + * + * @return BstoreContainer\BSE[] + */ + public function getBSECollection(): array + { + return $this->BSECollection; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php new file mode 100644 index 0000000..98e3656 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php @@ -0,0 +1,81 @@ +parent = $parent; + } + + public function getParent(): BstoreContainer + { + return $this->parent; + } + + /** + * Get the BLIP. + */ + public function getBlip(): ?BSE\Blip + { + return $this->blip; + } + + /** + * Set the BLIP. + */ + public function setBlip(BSE\Blip $blip): void + { + $this->blip = $blip; + $blip->setParent($this); + } + + /** + * Get the BLIP type. + */ + public function getBlipType(): int + { + return $this->blipType; + } + + /** + * Set the BLIP type. + */ + public function setBlipType(int $blipType): void + { + $this->blipType = $blipType; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php new file mode 100644 index 0000000..763bfe5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php @@ -0,0 +1,50 @@ +data; + } + + /** + * Set the raw image data. + */ + public function setData(string $data): void + { + $this->data = $data; + } + + /** + * Set parent BSE. + */ + public function setParent(BSE $parent): void + { + $this->parent = $parent; + } + + /** + * Get parent BSE. + */ + public function getParent(): BSE + { + return $this->parent; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/File.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/File.php new file mode 100644 index 0000000..022c1bb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/File.php @@ -0,0 +1,195 @@ +open($zipFile); + if ($res === true) { + $returnValue = ($zip->getFromName($archiveFile) !== false); + $zip->close(); + + return $returnValue; + } + } + + return false; + } + + return file_exists($filename); + } + + /** + * Returns canonicalized absolute pathname, also for ZIP archives. + */ + public static function realpath(string $filename): string + { + // Returnvalue + $returnValue = ''; + + // Try using realpath() + if (file_exists($filename)) { + $returnValue = realpath($filename) ?: ''; + } + + // Found something? + if ($returnValue === '') { + $pathArray = explode('/', $filename); + while (in_array('..', $pathArray) && $pathArray[0] != '..') { + $iMax = count($pathArray); + for ($i = 1; $i < $iMax; ++$i) { + if ($pathArray[$i] == '..') { + array_splice($pathArray, $i - 1, 2); + + break; + } + } + } + $returnValue = implode('/', $pathArray); + } + + // Return + return $returnValue; + } + + /** + * Get the systems temporary directory. + */ + public static function sysGetTempDir(): string + { + $path = sys_get_temp_dir(); + if (self::$useUploadTempDirectory) { + // use upload-directory when defined to allow running on environments having very restricted + // open_basedir configs + if (ini_get('upload_tmp_dir') !== false) { + if ($temp = ini_get('upload_tmp_dir')) { + if (file_exists($temp)) { + $path = $temp; + } + } + } + } + + return realpath($path) ?: ''; + } + + public static function temporaryFilename(): string + { + $filename = tempnam(self::sysGetTempDir(), 'phpspreadsheet'); + if ($filename === false) { + throw new Exception('Could not create temporary file'); + } + + return $filename; + } + + /** + * Assert that given path is an existing file and is readable, otherwise throw exception. + */ + public static function assertFile(string $filename, string $zipMember = ''): void + { + if (!is_file($filename)) { + throw new ReaderException('File "' . $filename . '" does not exist.'); + } + + if (!is_readable($filename)) { + throw new ReaderException('Could not open "' . $filename . '" for reading.'); + } + + if ($zipMember !== '') { + $zipfile = "zip://$filename#$zipMember"; + if (!self::fileExists($zipfile)) { + // Has the file been saved with Windoze directory separators rather than unix? + $zipfile = "zip://$filename#" . str_replace('/', '\\', $zipMember); + if (!self::fileExists($zipfile)) { + throw new ReaderException("Could not find zip member $zipfile"); + } + } + } + } + + /** + * Same as assertFile, except return true/false and don't throw Exception. + */ + public static function testFileNoThrow(string $filename, ?string $zipMember = null): bool + { + if (!is_file($filename)) { + return false; + } + if (!is_readable($filename)) { + return false; + } + if ($zipMember === null) { + return true; + } + // validate zip, but don't check specific member + if ($zipMember === '') { + return self::validateZipFirst4($filename); + } + + $zipfile = "zip://$filename#$zipMember"; + if (self::fileExists($zipfile)) { + return true; + } + + // Has the file been saved with Windoze directory separators rather than unix? + $zipfile = "zip://$filename#" . str_replace('/', '\\', $zipMember); + + return self::fileExists($zipfile); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Font.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Font.php new file mode 100644 index 0000000..3b0afbd --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Font.php @@ -0,0 +1,722 @@ + [ + 'x' => self::ARIAL, + 'xb' => self::ARIAL_BOLD, + 'xi' => self::ARIAL_ITALIC, + 'xbi' => self::ARIAL_BOLD_ITALIC, + ], + 'Calibri' => [ + 'x' => self::CALIBRI, + 'xb' => self::CALIBRI_BOLD, + 'xi' => self::CALIBRI_ITALIC, + 'xbi' => self::CALIBRI_BOLD_ITALIC, + ], + 'Comic Sans MS' => [ + 'x' => self::COMIC_SANS_MS, + 'xb' => self::COMIC_SANS_MS_BOLD, + 'xi' => self::COMIC_SANS_MS, + 'xbi' => self::COMIC_SANS_MS_BOLD, + ], + 'Courier New' => [ + 'x' => self::COURIER_NEW, + 'xb' => self::COURIER_NEW_BOLD, + 'xi' => self::COURIER_NEW_ITALIC, + 'xbi' => self::COURIER_NEW_BOLD_ITALIC, + ], + 'Georgia' => [ + 'x' => self::GEORGIA, + 'xb' => self::GEORGIA_BOLD, + 'xi' => self::GEORGIA_ITALIC, + 'xbi' => self::GEORGIA_BOLD_ITALIC, + ], + 'Impact' => [ + 'x' => self::IMPACT, + 'xb' => self::IMPACT, + 'xi' => self::IMPACT, + 'xbi' => self::IMPACT, + ], + 'Liberation Sans' => [ + 'x' => self::LIBERATION_SANS, + 'xb' => self::LIBERATION_SANS_BOLD, + 'xi' => self::LIBERATION_SANS_ITALIC, + 'xbi' => self::LIBERATION_SANS_BOLD_ITALIC, + ], + 'Lucida Console' => [ + 'x' => self::LUCIDA_CONSOLE, + 'xb' => self::LUCIDA_CONSOLE, + 'xi' => self::LUCIDA_CONSOLE, + 'xbi' => self::LUCIDA_CONSOLE, + ], + 'Lucida Sans Unicode' => [ + 'x' => self::LUCIDA_SANS_UNICODE, + 'xb' => self::LUCIDA_SANS_UNICODE, + 'xi' => self::LUCIDA_SANS_UNICODE, + 'xbi' => self::LUCIDA_SANS_UNICODE, + ], + 'Microsoft Sans Serif' => [ + 'x' => self::MICROSOFT_SANS_SERIF, + 'xb' => self::MICROSOFT_SANS_SERIF, + 'xi' => self::MICROSOFT_SANS_SERIF, + 'xbi' => self::MICROSOFT_SANS_SERIF, + ], + 'Palatino Linotype' => [ + 'x' => self::PALATINO_LINOTYPE, + 'xb' => self::PALATINO_LINOTYPE_BOLD, + 'xi' => self::PALATINO_LINOTYPE_ITALIC, + 'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC, + ], + 'Symbol' => [ + 'x' => self::SYMBOL, + 'xb' => self::SYMBOL, + 'xi' => self::SYMBOL, + 'xbi' => self::SYMBOL, + ], + 'Tahoma' => [ + 'x' => self::TAHOMA, + 'xb' => self::TAHOMA_BOLD, + 'xi' => self::TAHOMA, + 'xbi' => self::TAHOMA_BOLD, + ], + 'Times New Roman' => [ + 'x' => self::TIMES_NEW_ROMAN, + 'xb' => self::TIMES_NEW_ROMAN_BOLD, + 'xi' => self::TIMES_NEW_ROMAN_ITALIC, + 'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC, + ], + 'Trebuchet MS' => [ + 'x' => self::TREBUCHET_MS, + 'xb' => self::TREBUCHET_MS_BOLD, + 'xi' => self::TREBUCHET_MS_ITALIC, + 'xbi' => self::TREBUCHET_MS_BOLD_ITALIC, + ], + 'Verdana' => [ + 'x' => self::VERDANA, + 'xb' => self::VERDANA_BOLD, + 'xi' => self::VERDANA_ITALIC, + 'xbi' => self::VERDANA_BOLD_ITALIC, + ], + ]; + + /** + * Array that can be used to supplement FONT_FILE_NAMES for calculating exact width. + * + * @var array> + */ + private static array $extraFontArray = []; + + /** @param array> $extraFontArray */ + public static function setExtraFontArray(array $extraFontArray): void + { + self::$extraFontArray = $extraFontArray; + } + + /** @return array> */ + public static function getExtraFontArray(): array + { + return self::$extraFontArray; + } + + /** + * AutoSize method. + */ + private static string $autoSizeMethod = self::AUTOSIZE_METHOD_APPROX; + + /** + * Path to folder containing TrueType font .ttf files. + */ + private static string $trueTypeFontPath = ''; + + /** + * How wide is a default column for a given default font and size? + * Empirical data found by inspecting real Excel files and reading off the pixel width + * in Microsoft Office Excel 2007. + * Added height in points. + */ + public const DEFAULT_COLUMN_WIDTHS = [ + 'Arial' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + 11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5], + 9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25], + 10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75], + ], + ]; + + /** + * Set autoSize method. + * + * @param string $method see self::AUTOSIZE_METHOD_* + * + * @return bool Success or failure + */ + public static function setAutoSizeMethod(string $method): bool + { + if (!in_array($method, self::AUTOSIZE_METHODS)) { + return false; + } + self::$autoSizeMethod = $method; + + return true; + } + + /** + * Get autoSize method. + */ + public static function getAutoSizeMethod(): string + { + return self::$autoSizeMethod; + } + + /** + * Set the path to the folder containing .ttf files. There should be a trailing slash. + * Path will be recursively searched for font file. + * Typical locations on various platforms: + *
    + *
  • C:/Windows/Fonts/
  • + *
  • /usr/share/fonts/truetype/
  • + *
  • ~/.fonts/
  • + *
. + */ + public static function setTrueTypeFontPath(string $folderPath): void + { + self::$trueTypeFontPath = $folderPath; + } + + /** + * Get the path to the folder containing .ttf files. + */ + public static function getTrueTypeFontPath(): string + { + return self::$trueTypeFontPath; + } + + /** + * Pad amount for exact in pixels; use best guess if null. + */ + private static null|float|int $paddingAmountExact = null; + + /** + * Set pad amount for exact in pixels; use best guess if null. + */ + public static function setPaddingAmountExact(null|float|int $paddingAmountExact): void + { + self::$paddingAmountExact = $paddingAmountExact; + } + + /** + * Get pad amount for exact in pixels; or null if using best guess. + */ + public static function getPaddingAmountExact(): null|float|int + { + return self::$paddingAmountExact; + } + + /** + * Calculate an (approximate) OpenXML column width, based on font size and text contained. + * + * @param FontStyle $font Font object + * @param null|RichText|string $cellText Text to calculate width + * @param int $rotation Rotation angle + * @param null|FontStyle $defaultFont Font object + * @param bool $filterAdjustment Add space for Autofilter or Table dropdown + */ + public static function calculateColumnWidth( + FontStyle $font, + $cellText = '', + int $rotation = 0, + ?FontStyle $defaultFont = null, + bool $filterAdjustment = false, + int $indentAdjustment = 0 + ): float { + // If it is rich text, use plain text + if ($cellText instanceof RichText) { + $cellText = $cellText->getPlainText(); + } + + // Special case if there are one or more newline characters ("\n") + $cellText = (string) $cellText; + if (str_contains($cellText, "\n")) { + $lineTexts = explode("\n", $cellText); + $lineWidths = []; + foreach ($lineTexts as $lineText) { + $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont, $filterAdjustment); + } + + return max($lineWidths); // width of longest line in cell + } + + // Try to get the exact text width in pixels + $approximate = self::$autoSizeMethod === self::AUTOSIZE_METHOD_APPROX; + $columnWidth = 0; + if (!$approximate) { + try { + $columnWidthAdjust = ceil( + self::getTextWidthPixelsExact( + str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))), + $font, + 0 + ) * 1.07 + ); + + // Width of text in pixels excl. padding + // and addition because Excel adds some padding, just use approx width of 'n' glyph + $columnWidth = self::getTextWidthPixelsExact($cellText, $font, $rotation) + (self::$paddingAmountExact ?? $columnWidthAdjust); + } catch (PhpSpreadsheetException) { + $approximate = true; + } + } + + if ($approximate) { + $columnWidthAdjust = self::getTextWidthPixelsApprox( + str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))), + $font, + 0 + ); + // Width of text in pixels excl. padding, approximation + // and addition because Excel adds some padding, just use approx width of 'n' glyph + $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; + } + + // Convert from pixel width to column width + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle()); + + // Return + return round($columnWidth, 4); + } + + /** + * Get GD text width in pixels for a string of text in a certain font at a certain rotation angle. + */ + public static function getTextWidthPixelsExact(string $text, FontStyle $font, int $rotation = 0): float + { + // font size should really be supplied in pixels in GD2, + // but since GD2 seems to assume 72dpi, pixels and points are the same + $fontFile = self::getTrueTypeFontFileFromFont($font); + $textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text); + if ($textBox === false) { + // @codeCoverageIgnoreStart + throw new PhpSpreadsheetException('imagettfbbox failed'); + // @codeCoverageIgnoreEnd + } + + // Get corners positions + /** @var int[] $textBox */ + $lowerLeftCornerX = $textBox[0]; + $lowerRightCornerX = $textBox[2]; + $upperRightCornerX = $textBox[4]; + $upperLeftCornerX = $textBox[6]; + + // Consider the rotation when calculating the width + return round(max($lowerRightCornerX - $upperLeftCornerX, $upperRightCornerX - $lowerLeftCornerX), 4); + } + + /** + * Get approximate width in pixels for a string of text in a certain font at a certain rotation angle. + * + * @return int Text width in pixels (no padding added) + */ + public static function getTextWidthPixelsApprox(string $columnText, FontStyle $font, int $rotation = 0): int + { + $fontName = $font->getName(); + $fontSize = $font->getSize(); + + // Calculate column width in pixels. + // We assume fixed glyph width, but count double for "fullwidth" characters. + // Result varies with font name and size. + switch ($fontName) { + case 'Arial': + // value 8 was set because of experience in different exports at Arial 10 font. + $columnWidth = (int) (8 * StringHelper::countCharactersDbcs($columnText)); + $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + + break; + case 'Verdana': + // value 8 was found via interpolation by inspecting real Excel files with Verdana 10 font. + $columnWidth = (int) (8 * StringHelper::countCharactersDbcs($columnText)); + $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size + + break; + default: + // just assume Calibri + // value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font. + $columnWidth = (int) (8.26 * StringHelper::countCharactersDbcs($columnText)); + $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size + + break; + } + + // Calculate approximate rotated column width + if ($rotation !== 0) { + if ($rotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { + // stacked text + $columnWidth = 4; // approximation + } else { + // rotated text + $columnWidth = $columnWidth * cos(deg2rad($rotation)) + + $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation + } + } + + // pixel width is an integer + return (int) $columnWidth; + } + + /** + * Calculate an (approximate) pixel size, based on a font points size. + * + * @param float|int $fontSizeInPoints Font size (in points) + * + * @return int Font size (in pixels) + */ + public static function fontSizeToPixels(float|int $fontSizeInPoints): int + { + return (int) ((4 / 3) * $fontSizeInPoints); + } + + /** + * Calculate an (approximate) pixel size, based on inch size. + * + * @param float|int $sizeInInch Font size (in inch) + * + * @return float|int Size (in pixels) + */ + public static function inchSizeToPixels(int|float $sizeInInch): int|float + { + return $sizeInInch * 96; + } + + /** + * Calculate an (approximate) pixel size, based on centimeter size. + * + * @param float|int $sizeInCm Font size (in centimeters) + * + * @return float Size (in pixels) + */ + public static function centimeterSizeToPixels(int|float $sizeInCm): float + { + return $sizeInCm * 37.795275591; + } + + /** + * Returns the font path given the font. + * + * @return string Path to TrueType font file + */ + public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true): string + { + if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) { + throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); + } + + $name = $font->getName(); + $fontArray = array_merge(self::FONT_FILE_NAMES, self::$extraFontArray); + if (!isset($fontArray[$name])) { + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + } + $bold = $font->getBold(); + $italic = $font->getItalic(); + $index = 'x'; + if ($bold) { + $index .= 'b'; + } + if ($italic) { + $index .= 'i'; + } + $fontFile = $fontArray[$name][$index]; + + $separator = ''; + if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') { + $separator = DIRECTORY_SEPARATOR; + } + $fontFileAbsolute = preg_match('~^([A-Za-z]:)?[/\\\]~', $fontFile) === 1; + if (!$fontFileAbsolute) { + $fontFile = self::findFontFile(self::$trueTypeFontPath, $fontFile) ?? self::$trueTypeFontPath . $separator . $fontFile; + } + + // Check if file actually exists + if ($checkPath && !file_exists($fontFile) && !$fontFileAbsolute) { + $alternateName = $name; + if ($index !== 'x' && $fontArray[$name][$index] !== $fontArray[$name]['x']) { + // Bold but no italic: + // Comic Sans + // Tahoma + // Neither bold nor italic: + // Impact + // Lucida Console + // Lucida Sans Unicode + // Microsoft Sans Serif + // Symbol + if ($index === 'xb') { + $alternateName .= ' Bold'; + } elseif ($index === 'xi') { + $alternateName .= ' Italic'; + } elseif ($fontArray[$name]['xb'] === $fontArray[$name]['xbi']) { + $alternateName .= ' Bold'; + } else { + $alternateName .= ' Bold Italic'; + } + } + $fontFile = self::$trueTypeFontPath . $separator . $alternateName . '.ttf'; + if (!file_exists($fontFile)) { + throw new PhpSpreadsheetException('TrueType Font file not found'); + } + } + + return $fontFile; + } + + public const CHARSET_FROM_FONT_NAME = [ + 'EucrosiaUPC' => self::CHARSET_ANSI_THAI, + 'Wingdings' => self::CHARSET_SYMBOL, + 'Wingdings 2' => self::CHARSET_SYMBOL, + 'Wingdings 3' => self::CHARSET_SYMBOL, + ]; + + /** + * Returns the associated charset for the font name. + * + * @param string $fontName Font name + * + * @return int Character set code + */ + public static function getCharsetFromFontName(string $fontName): int + { + return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN; + } + + /** + * Get the effective column width for columns without a column dimension or column with width -1 + * For example, for Calibri 11 this is 9.140625 (64 px). + * + * @param FontStyle $font The workbooks default font + * @param bool $returnAsPixels true = return column width in pixels, false = return in OOXML units + * + * @return ($returnAsPixels is true ? int : float) Column width + */ + public static function getDefaultColumnWidthByFont(FontStyle $font, bool $returnAsPixels = false): float|int + { + $size = $font->getSize(); + $sizex = ($size !== null && $size == (int) $size) ? ((int) $size) : "$size"; + if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$sizex])) { + // Exact width can be determined + $columnWidth = $returnAsPixels + ? self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$sizex]['px'] + : self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$sizex]['width']; + } else { + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + $columnWidth = $returnAsPixels + ? self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] + : self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width']; + $columnWidth = $columnWidth * $font->getSize() / 11; + + // Round pixels to closest integer + if ($returnAsPixels) { + $columnWidth = (int) round($columnWidth); + } + } + + return $columnWidth; + } + + /** + * Get the effective row height for rows without a row dimension or rows with height -1 + * For example, for Calibri 11 this is 15 points. + * + * @param FontStyle $font The workbooks default font + * + * @return float Row height in points + */ + public static function getDefaultRowHeightByFont(FontStyle $font): float + { + $name = $font->getName(); + $size = $font->getSize(); + $sizex = ($size !== null && $size == (int) $size) ? ((int) $size) : "$size"; + if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$sizex])) { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$sizex]['height']; + } elseif ($name === 'Arial' || $name === 'Verdana') { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0; + } else { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0; + } + + return $rowHeight; + } + + private static function findFontFile(string $startDirectory, string $desiredFont): ?string + { + $fontPath = null; + if ($startDirectory === '') { + return null; + } + if (file_exists("$startDirectory/$desiredFont")) { + $fontPath = "$startDirectory/$desiredFont"; + } else { + $iterations = 0; + $it = new RecursiveDirectoryIterator( + $startDirectory, + RecursiveDirectoryIterator::SKIP_DOTS + | RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ); + foreach ( + new RecursiveIteratorIterator( + $it, + RecursiveIteratorIterator::LEAVES_ONLY, + RecursiveIteratorIterator::CATCH_GET_CHILD + ) as $filex + ) { + /** @var string */ + $file = $filex; + if (basename($file) === $desiredFont) { + $fontPath = $file; + + break; + } + ++$iterations; + if ($iterations > 5000) { + // @codeCoverageIgnoreStart + break; + // @codeCoverageIgnoreEnd + } + } + } + + return $fontPath; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/IntOrFloat.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/IntOrFloat.php new file mode 100644 index 0000000..2e20c9c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/IntOrFloat.php @@ -0,0 +1,17 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// + +use PhpOffice\PhpSpreadsheet\Exception; +use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; +use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root; + +/* + * Array for storing OLE instances that are accessed from + * OLE_ChainedBlockStream::stream_open(). + * + * @var array + */ +$GLOBALS['_OLE_INSTANCES'] = []; + +/** + * OLE package base class. + * + * @author Xavier Noguer + * @author Christian Schmidt + */ +class OLE +{ + const OLE_PPS_TYPE_ROOT = 5; + const OLE_PPS_TYPE_DIR = 1; + const OLE_PPS_TYPE_FILE = 2; + const OLE_DATA_SIZE_SMALL = 0x1000; + const OLE_LONG_INT_SIZE = 4; + const OLE_PPS_SIZE = 0x80; + + /** + * The file handle for reading an OLE container. + * + * @var resource + */ + public $_file_handle; + + /** + * Array of PPS's found on the OLE container. + * + * @var array + */ + public array $_list = []; + + /** + * Root directory of OLE container. + */ + public Root $root; + + /** + * Big Block Allocation Table. + * + * @var mixed[] (blockId => nextBlockId) + */ + public array $bbat; + + /** + * Short Block Allocation Table. + * + * @var mixed[] (blockId => nextBlockId) + */ + public array $sbat; + + /** + * Size of big blocks. This is usually 512. + * + * @var int<1, max> number of octets per block + */ + public int $bigBlockSize; + + /** + * Size of small blocks. This is usually 64. + * + * @var int number of octets per block + */ + public int $smallBlockSize; + + /** + * Threshold for big blocks. + */ + public int $bigBlockThreshold; + + /** + * Reads an OLE container from the contents of the file given. + * + * @acces public + * + * @return bool true on success, PEAR_Error on failure + */ + public function read(string $filename): bool + { + $fh = @fopen($filename, 'rb'); + if ($fh === false) { + throw new ReaderException("Can't open file $filename"); + } + $this->_file_handle = $fh; + + $signature = fread($fh, 8); + if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) { + throw new ReaderException("File doesn't seem to be an OLE container."); + } + fseek($fh, 28); + if (fread($fh, 2) != "\xFE\xFF") { + // This shouldn't be a problem in practice + throw new ReaderException('Only Little-Endian encoding is supported.'); + } + // Size of blocks and short blocks in bytes + /** @var int<1, max> */ + $temp = 2 ** self::readInt2($fh); + $this->bigBlockSize = $temp; + $this->smallBlockSize = 2 ** self::readInt2($fh); + + // Skip UID, revision number and version number + fseek($fh, 44); + // Number of blocks in Big Block Allocation Table + $bbatBlockCount = self::readInt4($fh); + + // Root chain 1st block + $directoryFirstBlockId = self::readInt4($fh); + + // Skip unused bytes + fseek($fh, 56); + // Streams shorter than this are stored using small blocks + $this->bigBlockThreshold = self::readInt4($fh); + // Block id of first sector in Short Block Allocation Table + $sbatFirstBlockId = self::readInt4($fh); + // Number of blocks in Short Block Allocation Table + $sbbatBlockCount = self::readInt4($fh); + // Block id of first sector in Master Block Allocation Table + $mbatFirstBlockId = self::readInt4($fh); + // Number of blocks in Master Block Allocation Table + $mbbatBlockCount = self::readInt4($fh); + $this->bbat = []; + + // Remaining 4 * 109 bytes of current block is beginning of Master + // Block Allocation Table + $mbatBlocks = []; + for ($i = 0; $i < 109; ++$i) { + $mbatBlocks[] = self::readInt4($fh); + } + + // Read rest of Master Block Allocation Table (if any is left) + $pos = $this->getBlockOffset($mbatFirstBlockId); + for ($i = 0; $i < $mbbatBlockCount; ++$i) { + fseek($fh, $pos); + for ($j = 0; $j < $this->bigBlockSize / 4 - 1; ++$j) { + $mbatBlocks[] = self::readInt4($fh); + } + // Last block id in each block points to next block + $pos = $this->getBlockOffset(self::readInt4($fh)); + } + + // Read Big Block Allocation Table according to chain specified by $mbatBlocks + for ($i = 0; $i < $bbatBlockCount; ++$i) { + $pos = $this->getBlockOffset($mbatBlocks[$i]); + fseek($fh, $pos); + for ($j = 0; $j < $this->bigBlockSize / 4; ++$j) { + $this->bbat[] = self::readInt4($fh); + } + } + + // Read short block allocation table (SBAT) + $this->sbat = []; + $shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4; + $sbatFh = $this->getStream($sbatFirstBlockId); + for ($blockId = 0; $blockId < $shortBlockCount; ++$blockId) { + $this->sbat[$blockId] = self::readInt4($sbatFh); + } + fclose($sbatFh); + + $this->readPpsWks($directoryFirstBlockId); + + return true; + } + + /** + * @param int $blockId byte offset from beginning of file + */ + public function getBlockOffset(int $blockId): int + { + return 512 + $blockId * $this->bigBlockSize; + } + + /** + * Returns a stream for use with fread() etc. External callers should + * use \PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File::getStream(). + * + * @param int|OLE\PPS $blockIdOrPps block id or PPS + * + * @return resource read-only stream + */ + public function getStream($blockIdOrPps) + { + static $isRegistered = false; + if (!$isRegistered) { + stream_wrapper_register('ole-chainedblockstream', ChainedBlockStream::class); + $isRegistered = true; + } + + // Store current instance in global array, so that it can be accessed + // in OLE_ChainedBlockStream::stream_open(). + // Object is removed from self::$instances in OLE_Stream::close(). + $GLOBALS['_OLE_INSTANCES'][] = $this; //* @phpstan-ignore-line + $keys = array_keys($GLOBALS['_OLE_INSTANCES']); //* @phpstan-ignore-line + $instanceId = end($keys); + + $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId; + if ($blockIdOrPps instanceof OLE\PPS) { + $path .= '&blockId=' . $blockIdOrPps->startBlock; + $path .= '&size=' . $blockIdOrPps->Size; + } else { + $path .= '&blockId=' . $blockIdOrPps; + } + + $resource = fopen($path, 'rb'); + if ($resource === false) { + throw new Exception("Unable to open stream $path"); + } + + return $resource; + } + + /** + * Reads a signed char. + * + * @param resource $fileHandle file handle + */ + private static function readInt1($fileHandle): int + { + [, $tmp] = unpack('c', fread($fileHandle, 1) ?: '') ?: [0, 0]; + /** @var int $tmp */ + + return $tmp; + } + + /** + * Reads an unsigned short (2 octets). + * + * @param resource $fileHandle file handle + */ + private static function readInt2($fileHandle): int + { + [, $tmp] = unpack('v', fread($fileHandle, 2) ?: '') ?: [0, 0]; + /** @var int $tmp */ + + return $tmp; + } + + private const SIGNED_4OCTET_LIMIT = 2147483648; + + private const SIGNED_4OCTET_SUBTRACT = 2 * self::SIGNED_4OCTET_LIMIT; + + /** + * Reads long (4 octets), interpreted as if signed on 32-bit system. + * + * @param resource $fileHandle file handle + */ + private static function readInt4($fileHandle): int + { + [, $tmp] = unpack('V', fread($fileHandle, 4) ?: '') ?: [0, 0]; + /** @var int $tmp */ + if ($tmp >= self::SIGNED_4OCTET_LIMIT) { + $tmp -= self::SIGNED_4OCTET_SUBTRACT; + } + + return $tmp; + } + + /** + * Gets information about all PPS's on the OLE container from the PPS WK's + * creates an OLE_PPS object for each one. + * + * @param int $blockId the block id of the first block + * + * @return bool true on success, PEAR_Error on failure + */ + public function readPpsWks(int $blockId): bool + { + $fh = $this->getStream($blockId); + for ($pos = 0; true; $pos += 128) { + fseek($fh, $pos, SEEK_SET); + $nameUtf16 = (string) fread($fh, 64); + $nameLength = self::readInt2($fh); + $nameUtf16 = substr($nameUtf16, 0, $nameLength - 2); + // Simple conversion from UTF-16LE to ISO-8859-1 + $name = str_replace("\x00", '', $nameUtf16); + $type = self::readInt1($fh); + switch ($type) { + case self::OLE_PPS_TYPE_ROOT: + $pps = new Root(null, null, []); + $this->root = $pps; + + break; + case self::OLE_PPS_TYPE_DIR: + $pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []); + + break; + case self::OLE_PPS_TYPE_FILE: + $pps = new OLE\PPS\File($name); + + break; + default: + throw new Exception('Unsupported PPS type'); + } + fseek($fh, 1, SEEK_CUR); + $pps->Type = $type; + $pps->Name = $name; + $pps->PrevPps = self::readInt4($fh); + $pps->NextPps = self::readInt4($fh); + $pps->DirPps = self::readInt4($fh); + fseek($fh, 20, SEEK_CUR); + $pps->Time1st = self::OLE2LocalDate((string) fread($fh, 8)); + $pps->Time2nd = self::OLE2LocalDate((string) fread($fh, 8)); + $pps->startBlock = self::readInt4($fh); + $pps->Size = self::readInt4($fh); + $pps->No = count($this->_list); + $this->_list[] = $pps; + + // check if the PPS tree (starting from root) is complete + if (isset($this->root) && $this->ppsTreeComplete($this->root->No)) { + break; + } + } + fclose($fh); + + // Initialize $pps->children on directories + foreach ($this->_list as $pps) { + if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) { + $nos = [$pps->DirPps]; + $pps->children = []; + while (!empty($nos)) { + $no = array_pop($nos); + if ($no != -1) { + $childPps = $this->_list[$no]; + $nos[] = $childPps->PrevPps; + $nos[] = $childPps->NextPps; + $pps->children[] = $childPps; + } + } + } + } + + return true; + } + + /** + * It checks whether the PPS tree is complete (all PPS's read) + * starting with the given PPS (not necessarily root). + * + * @param int $index The index of the PPS from which we are checking + * + * @return bool Whether the PPS tree for the given PPS is complete + */ + private function ppsTreeComplete(int $index): bool + { + if (!isset($this->_list[$index])) { + return false; + } + $pps = $this->_list[$index]; + + return + ($pps->PrevPps == -1 + || $this->ppsTreeComplete($pps->PrevPps)) + && ($pps->NextPps == -1 + || $this->ppsTreeComplete($pps->NextPps)) + && ($pps->DirPps == -1 + || $this->ppsTreeComplete($pps->DirPps)); + } + + /** + * Checks whether a PPS is a File PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @param int $index The index for the PPS + * + * @return bool true if it's a File PPS, false otherwise + */ + public function isFile(int $index): bool + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Type == self::OLE_PPS_TYPE_FILE; + } + + return false; + } + + /** + * Checks whether a PPS is a Root PPS or not. + * If there is no PPS for the index given, it will return false. + * + * @param int $index the index for the PPS + * + * @return bool true if it's a Root PPS, false otherwise + */ + public function isRoot(int $index): bool + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Type == self::OLE_PPS_TYPE_ROOT; + } + + return false; + } + + /** + * Gives the total number of PPS's found in the OLE container. + * + * @return int The total number of PPS's found in the OLE container + */ + public function ppsTotal(): int + { + return count($this->_list); + } + + /** + * Gets data from a PPS + * If there is no PPS for the index given, it will return an empty string. + * + * @param int $index The index for the PPS + * @param int $position The position from which to start reading + * (relative to the PPS) + * @param int $length The amount of bytes to read (at most) + * + * @return string The binary string containing the data requested + * + * @see OLE_PPS_File::getStream() + */ + public function getData(int $index, int $position, int $length): string + { + // if position is not valid return empty string + if (!isset($this->_list[$index]) || ($position >= $this->_list[$index]->Size) || ($position < 0)) { + return ''; + } + $fh = $this->getStream($this->_list[$index]); + $data = (string) stream_get_contents($fh, $length, $position); + fclose($fh); + + return $data; + } + + /** + * Gets the data length from a PPS + * If there is no PPS for the index given, it will return 0. + * + * @param int $index The index for the PPS + * + * @return int The amount of bytes in data the PPS has + */ + public function getDataLength(int $index): int + { + if (isset($this->_list[$index])) { + return $this->_list[$index]->Size; + } + + return 0; + } + + /** + * Utility function to transform ASCII text to Unicode. + * + * @param string $ascii The ASCII string to transform + * + * @return string The string in Unicode + */ + public static function ascToUcs(string $ascii): string + { + $rawname = ''; + $iMax = strlen($ascii); + for ($i = 0; $i < $iMax; ++$i) { + $rawname .= $ascii[$i] + . "\x00"; + } + + return $rawname; + } + + /** + * Utility function + * Returns a string for the OLE container with the date given. + * + * @param float|int $date A timestamp + * + * @return string The string for the OLE container + */ + public static function localDateToOLE($date): string + { + if (!$date) { + return "\x00\x00\x00\x00\x00\x00\x00\x00"; + } + $dateTime = Date::dateTimeFromTimestamp("$date"); + + // days from 1-1-1601 until the beginning of UNIX era + $days = 134774; + // calculate seconds + $big_date = $days * 24 * 3600 + (float) $dateTime->format('U'); + // multiply just to make MS happy + $big_date *= 10000000; + + // Make HEX string + $res = ''; + + $factor = 2 ** 56; + while ($factor >= 1) { + $hex = (int) floor($big_date / $factor); + $res = pack('c', $hex) . $res; + $big_date = fmod($big_date, $factor); + $factor /= 256; + } + + return $res; + } + + /** + * Returns a timestamp from an OLE container's date. + * + * @param string $oleTimestamp A binary string with the encoded date + * + * @return float|int The Unix timestamp corresponding to the string + */ + public static function OLE2LocalDate(string $oleTimestamp) + { + if (strlen($oleTimestamp) != 8) { + throw new ReaderException('Expecting 8 byte string'); + } + + // convert to units of 100 ns since 1601: + /** @var int[] */ + $unpackedTimestamp = unpack('v4', $oleTimestamp) ?: []; + $timestampHigh = (float) $unpackedTimestamp[4] * 65536 + (float) $unpackedTimestamp[3]; + $timestampLow = (float) $unpackedTimestamp[2] * 65536 + (float) $unpackedTimestamp[1]; + + // translate to seconds since 1601: + $timestampHigh /= 10000000; + $timestampLow /= 10000000; + + // days from 1601 to 1970: + $days = 134774; + + // translate to seconds since 1970: + $unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5); + + return IntOrFloat::evaluate($unixTimestamp); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php new file mode 100644 index 0000000..76f1be7 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php @@ -0,0 +1,197 @@ +params); + if (!isset($this->params['oleInstanceId'], $this->params['blockId'], $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']])) { //* @phpstan-ignore-line + if ($options & STREAM_REPORT_ERRORS) { + trigger_error('OLE stream not found', E_USER_WARNING); + } + + return false; + } + $this->ole = $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']]; //* @phpstan-ignore-line + if (!($this->ole instanceof OLE)) { //* @phpstan-ignore-line + throw new Exception('class is not OLE'); + } + + $blockId = $this->params['blockId']; + $this->data = ''; + if (isset($this->params['size']) && $this->params['size'] < $this->ole->bigBlockThreshold && $blockId != $this->ole->root->startBlock) { + // Block id refers to small blocks + $rootPos = $this->ole->getBlockOffset((int) $this->ole->root->startBlock); + while ($blockId != -2) { + /** @var int $blockId */ + $pos = $rootPos + $blockId * $this->ole->bigBlockSize; + $blockId = $this->ole->sbat[$blockId]; + fseek($this->ole->_file_handle, $pos); + $this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize); + } + } else { + // Block id refers to big blocks + while ($blockId != -2) { + /** @var int $blockId */ + $pos = $this->ole->getBlockOffset($blockId); + fseek($this->ole->_file_handle, $pos); + $this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize); + $blockId = $this->ole->bbat[$blockId]; + } + } + if (isset($this->params['size'])) { + $this->data = substr($this->data, 0, $this->params['size']); //* @phpstan-ignore-line + } + + if ($options & STREAM_USE_PATH) { + $openedPath = $path; + } + + return true; + } + + /** + * Implements support for fclose(). + */ + public function stream_close(): void // @codingStandardsIgnoreLine + { + $this->ole = null; + unset($GLOBALS['_OLE_INSTANCES']); + } + + /** + * Implements support for fread(), fgets() etc. + * + * @param int $count maximum number of bytes to read + * + * @return false|string + */ + public function stream_read(int $count): bool|string // @codingStandardsIgnoreLine + { + if ($this->stream_eof()) { + return false; + } + $s = substr($this->data, (int) $this->pos, $count); + $this->pos += $count; + + return $s; + } + + /** + * Implements support for feof(). + * + * @return bool TRUE if the file pointer is at EOF; otherwise FALSE + */ + public function stream_eof(): bool // @codingStandardsIgnoreLine + { + return $this->pos >= strlen($this->data); + } + + /** + * Returns the position of the file pointer, i.e. its offset into the file + * stream. Implements support for ftell(). + */ + public function stream_tell(): int // @codingStandardsIgnoreLine + { + return $this->pos; + } + + /** + * Implements support for fseek(). + * + * @param int $offset byte offset + * @param int $whence SEEK_SET, SEEK_CUR or SEEK_END + */ + public function stream_seek(int $offset, int $whence): bool // @codingStandardsIgnoreLine + { + if ($whence == SEEK_SET && $offset >= 0) { + $this->pos = $offset; + } elseif ($whence == SEEK_CUR && -$offset <= $this->pos) { + $this->pos += $offset; + } elseif ($whence == SEEK_END && -$offset <= count($this->data)) { // @phpstan-ignore-line + $this->pos = strlen($this->data) + $offset; + } else { + return false; + } + + return true; + } + + /** + * Implements support for fstat(). Currently the only supported field is + * "size". + * + * @return array{size: int} + */ + public function stream_stat(): array // @codingStandardsIgnoreLine + { + return [ + 'size' => strlen($this->data), + ]; + } + + // Methods used by stream_wrapper_register() that are not implemented: + // bool stream_flush ( void ) + // int stream_write ( string data ) + // bool rename ( string path_from, string path_to ) + // bool mkdir ( string path, int mode, int options ) + // bool rmdir ( string path, int options ) + // bool dir_opendir ( string path, int options ) + // array url_stat ( string path, int flags ) + // string dir_readdir ( void ) + // bool dir_rewinddir ( void ) + // bool dir_closedir ( void ) +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS.php new file mode 100644 index 0000000..dae381b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS.php @@ -0,0 +1,211 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; + +/** + * Class for creating PPS's for OLE containers. + * + * @author Xavier Noguer + */ +class PPS +{ + private const ALL_ONE_BITS = (PHP_INT_SIZE > 4) ? 0xFFFFFFFF : -1; + + /** + * The PPS index. + */ + public int $No; + + /** + * The PPS name (in Unicode). + */ + public string $Name; + + /** + * The PPS type. Dir, Root or File. + */ + public int $Type; + + /** + * The index of the previous PPS. + */ + public int $PrevPps; + + /** + * The index of the next PPS. + */ + public int $NextPps; + + /** + * The index of it's first child if this is a Dir or Root PPS. + */ + public int $DirPps; + + /** + * A timestamp. + */ + public float|int $Time1st; + + /** + * A timestamp. + */ + public float|int $Time2nd; + + /** + * Starting block (small or big) for this PPS's data inside the container. + */ + public ?int $startBlock = null; + + /** + * The size of the PPS's data (in bytes). + */ + public int $Size; + + /** + * The PPS's data (only used if it's not using a temporary file). + */ + public string $_data = ''; + + /** + * Array of child PPS's (only used by Root and Dir PPS's). + * + * @var mixed[] + */ + public array $children = []; + + /** + * Pointer to OLE container. + */ + public OLE $ole; + + /** + * The constructor. + * + * @param ?int $No The PPS index + * @param ?string $name The PPS name + * @param ?int $type The PPS type. Dir, Root or File + * @param ?int $prev The index of the previous PPS + * @param ?int $next The index of the next PPS + * @param ?int $dir The index of it's first child if this is a Dir or Root PPS + * @param null|float|int $time_1st A timestamp + * @param null|float|int $time_2nd A timestamp + * @param ?string $data The (usually binary) source data of the PPS + * @param mixed[] $children Array containing children PPS for this PPS + */ + public function __construct(?int $No, ?string $name, ?int $type, ?int $prev, ?int $next, ?int $dir, $time_1st, $time_2nd, ?string $data, array $children) + { + $this->No = (int) $No; + $this->Name = (string) $name; + $this->Type = (int) $type; + $this->PrevPps = (int) $prev; + $this->NextPps = (int) $next; + $this->DirPps = (int) $dir; + $this->Time1st = $time_1st ?? 0; + $this->Time2nd = $time_2nd ?? 0; + $this->_data = (string) $data; + $this->children = $children; + $this->Size = strlen((string) $data); + } + + /** + * Returns the amount of data saved for this PPS. + * + * @return int The amount of data (in bytes) + */ + public function getDataLen(): int + { + //if (!isset($this->_data)) { + // return 0; + //} + + return strlen($this->_data); + } + + /** + * Returns a string with the PPS's WK (What is a WK?). + * + * @return string The binary string + */ + public function getPpsWk(): string + { + $ret = str_pad($this->Name, 64, "\x00"); + + $ret .= pack('v', strlen($this->Name) + 2) // 66 + . pack('c', $this->Type) // 67 + . pack('c', 0x00) //UK // 68 + . pack('V', $this->PrevPps) //Prev // 72 + . pack('V', $this->NextPps) //Next // 76 + . pack('V', $this->DirPps) //Dir // 80 + . "\x00\x09\x02\x00" // 84 + . "\x00\x00\x00\x00" // 88 + . "\xc0\x00\x00\x00" // 92 + . "\x00\x00\x00\x46" // 96 // Seems to be ok only for Root + . "\x00\x00\x00\x00" // 100 + . OLE::localDateToOLE($this->Time1st) // 108 + . OLE::localDateToOLE($this->Time2nd) // 116 + . pack('V', $this->startBlock ?? 0) // 120 + . pack('V', $this->Size) // 124 + . pack('V', 0); // 128 + + return $ret; + } + + /** + * Updates index and pointers to previous, next and children PPS's for this + * PPS. I don't think it'll work with Dir PPS's. + * + * @param self[] $raList Reference to the array of PPS's for the whole OLE + * container + * + * @return int The index for this PPS + */ + public static function savePpsSetPnt(array &$raList, mixed $to_save, int $depth = 0): int + { + if (!is_array($to_save) || (empty($to_save))) { + return self::ALL_ONE_BITS; + } + /** @var self[] $to_save */ + if (count($to_save) == 1) { + $cnt = count($raList); + // If the first entry, it's the root... Don't clone it! + $raList[$cnt] = ($depth == 0) ? $to_save[0] : clone $to_save[0]; + $raList[$cnt]->No = $cnt; + $raList[$cnt]->PrevPps = self::ALL_ONE_BITS; + $raList[$cnt]->NextPps = self::ALL_ONE_BITS; + $raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++); + } else { + $iPos = (int) floor(count($to_save) / 2); + $aPrev = array_slice($to_save, 0, $iPos); + $aNext = array_slice($to_save, $iPos + 1); + $cnt = count($raList); + // If the first entry, it's the root... Don't clone it! + $raList[$cnt] = ($depth == 0) ? $to_save[$iPos] : clone $to_save[$iPos]; + $raList[$cnt]->No = $cnt; + $raList[$cnt]->PrevPps = self::savePpsSetPnt($raList, $aPrev, $depth++); + $raList[$cnt]->NextPps = self::savePpsSetPnt($raList, $aNext, $depth++); + $raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++); + } + + return $cnt; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS/File.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS/File.php new file mode 100644 index 0000000..0798e3b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS/File.php @@ -0,0 +1,62 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; + +/** + * Class for creating File PPS's for OLE containers. + * + * @author Xavier Noguer + */ +class File extends PPS +{ + /** + * The constructor. + * + * @param string $name The name of the file (in Unicode) + * + * @see OLE::ascToUcs() + */ + public function __construct(string $name) + { + parent::__construct(null, $name, OLE::OLE_PPS_TYPE_FILE, null, null, null, null, null, '', []); + } + + /** + * Initialization method. Has to be called right after OLE_PPS_File(). + */ + public function init(): bool + { + return true; + } + + /** + * Append data to PPS. + * + * @param string $data The data to append + */ + public function append(string $data): void + { + $this->_data .= $data; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php new file mode 100644 index 0000000..c92deb5 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php @@ -0,0 +1,406 @@ + | +// | Based on OLE::Storage_Lite by Kawai, Takanori | +// +----------------------------------------------------------------------+ +// +use PhpOffice\PhpSpreadsheet\Shared\OLE; +use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; + +/** + * Class for creating Root PPS's for OLE containers. + * + * @author Xavier Noguer + */ +class Root extends PPS +{ + /** + * @var resource + */ + private $fileHandle; + + private ?int $smallBlockSize = null; + + private ?int $bigBlockSize = null; + + /** + * @param null|float|int $time_1st A timestamp + * @param null|float|int $time_2nd A timestamp + * @param File[] $raChild + */ + public function __construct($time_1st, $time_2nd, array $raChild) + { + parent::__construct(null, OLE::ascToUcs('Root Entry'), OLE::OLE_PPS_TYPE_ROOT, null, null, null, $time_1st, $time_2nd, null, $raChild); + } + + /** + * Method for saving the whole OLE container (including files). + * In fact, if called with an empty argument (or '-'), it saves to a + * temporary file and then outputs it's contents to stdout. + * If a resource pointer to a stream created by fopen() is passed + * it will be used, but you have to close such stream by yourself. + * + * @param resource $fileHandle the name of the file or stream where to save the OLE container + * + * @return bool true on success + */ + public function save($fileHandle): bool + { + $this->fileHandle = $fileHandle; + + // Initial Setting for saving + $this->bigBlockSize = (int) (2 ** ( + (isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9 + )); + $this->smallBlockSize = (int) (2 ** ( + (isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6 + )); + + // Make an array of PPS's (for Save) + $aList = []; + PPS::savePpsSetPnt($aList, [$this]); + // calculate values for header + [$iSBDcnt, $iBBcnt, $iPPScnt] = $this->calcSize($aList); //, $rhInfo); + // Save Header + $this->saveHeader((int) $iSBDcnt, (int) $iBBcnt, (int) $iPPScnt); + + // Make Small Data string (write SBD) + $this->_data = $this->makeSmallData($aList); + + // Write BB + $this->saveBigData((int) $iSBDcnt, $aList); + // Write PPS + $this->savePps($aList); + // Write Big Block Depot and BDList and Adding Header informations + $this->saveBbd((int) $iSBDcnt, (int) $iBBcnt, (int) $iPPScnt); + + return true; + } + + /** + * Calculate some numbers. + * + * @param PPS[] $raList Reference to an array of PPS's + * + * @return float[] The array of numbers + */ + private function calcSize(array &$raList): array + { + // Calculate Basic Setting + [$iSBDcnt, $iBBcnt, $iPPScnt] = [0, 0, 0]; + $iSBcnt = 0; + $iCount = count($raList); + for ($i = 0; $i < $iCount; ++$i) { + if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) { + $raList[$i]->Size = $raList[$i]->getDataLen(); + if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) { + $iSBcnt += floor($raList[$i]->Size / $this->smallBlockSize) + + (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0); + } else { + $iBBcnt += (floor($raList[$i]->Size / $this->bigBlockSize) + + (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0)); + } + } + } + $iSmallLen = $iSBcnt * $this->smallBlockSize; + $iSlCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE); + $iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt) ? 1 : 0); + $iBBcnt += (floor($iSmallLen / $this->bigBlockSize) + + (($iSmallLen % $this->bigBlockSize) ? 1 : 0)); + $iCnt = count($raList); + $iBdCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE; + $iPPScnt = (floor($iCnt / $iBdCnt) + (($iCnt % $iBdCnt) ? 1 : 0)); + + return [$iSBDcnt, $iBBcnt, $iPPScnt]; + } + + /** + * Helper function for calculating a magic value for block sizes. + * + * @param int $i2 The argument + * + * @see save() + */ + private static function adjust2(int $i2): float + { + $iWk = log($i2) / log(2); + + return ($iWk > floor($iWk)) ? floor($iWk) + 1 : $iWk; + } + + /** + * Save OLE header. + */ + private function saveHeader(int $iSBDcnt, int $iBBcnt, int $iPPScnt): void + { + $FILE = $this->fileHandle; + + // Calculate Basic Setting + $iBlCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE; + $i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE; + + $iBdExL = 0; + $iAll = $iBBcnt + $iPPScnt + $iSBDcnt; + $iAllW = $iAll; + $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0); + $iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0); + + // Calculate BD count + if ($iBdCnt > $i1stBdL) { + while (1) { + ++$iBdExL; + ++$iAllW; + $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0); + $iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0); + if ($iBdCnt <= ($iBdExL * $iBlCnt + $i1stBdL)) { + break; + } + } + } + + // Save Header + fwrite( + $FILE, + "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . pack('v', 0x3B) + . pack('v', 0x03) + . pack('v', -2) + . pack('v', 9) + . pack('v', 6) + . pack('v', 0) + . "\x00\x00\x00\x00" + . "\x00\x00\x00\x00" + . pack('V', $iBdCnt) + . pack('V', $iBBcnt + $iSBDcnt) //ROOT START + . pack('V', 0) + . pack('V', 0x1000) + . pack('V', $iSBDcnt ? 0 : -2) //Small Block Depot + . pack('V', $iSBDcnt) + ); + // Extra BDList Start, Count + if ($iBdCnt < $i1stBdL) { + fwrite( + $FILE, + pack('V', -2) // Extra BDList Start + . pack('V', 0)// Extra BDList Count + ); + } else { + fwrite($FILE, pack('V', $iAll + $iBdCnt) . pack('V', $iBdExL)); + } + + // BDList + for ($i = 0; $i < $i1stBdL && $i < $iBdCnt; ++$i) { + fwrite($FILE, pack('V', $iAll + $i)); + } + if ($i < $i1stBdL) { + $jB = $i1stBdL - $i; + for ($j = 0; $j < $jB; ++$j) { + fwrite($FILE, (pack('V', -1))); + } + } + } + + /** + * Saving big data (PPS's with data bigger than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). + * + * @param PPS[] $raList Reference to array of PPS's + */ + private function saveBigData(int $iStBlk, array &$raList): void + { + $FILE = $this->fileHandle; + + // cycle through PPS's + $iCount = count($raList); + for ($i = 0; $i < $iCount; ++$i) { + if ($raList[$i]->Type != OLE::OLE_PPS_TYPE_DIR) { + $raList[$i]->Size = $raList[$i]->getDataLen(); + if (($raList[$i]->Size >= OLE::OLE_DATA_SIZE_SMALL) || (($raList[$i]->Type == OLE::OLE_PPS_TYPE_ROOT) && isset($raList[$i]->_data))) { + fwrite($FILE, $raList[$i]->_data); + + if ($raList[$i]->Size % $this->bigBlockSize) { + fwrite($FILE, str_repeat("\x00", $this->bigBlockSize - ($raList[$i]->Size % $this->bigBlockSize))); + } + // Set For PPS + $raList[$i]->startBlock = $iStBlk; + $iStBlk + += ((int) floor($raList[$i]->Size / $this->bigBlockSize) + + (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0)); + } + } + } + } + + /** + * get small data (PPS's with data smaller than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). + * + * @param PPS[] $raList Reference to array of PPS's + */ + private function makeSmallData(array &$raList): string + { + $sRes = ''; + $FILE = $this->fileHandle; + $iSmBlk = 0; + + $iCount = count($raList); + for ($i = 0; $i < $iCount; ++$i) { + // Make SBD, small data string + if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) { + if ($raList[$i]->Size <= 0) { + continue; + } + if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) { + $iSmbCnt = (int) floor($raList[$i]->Size / $this->smallBlockSize) + + (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0); + // Add to SBD + $jB = $iSmbCnt - 1; + for ($j = 0; $j < $jB; ++$j) { + fwrite($FILE, pack('V', $j + $iSmBlk + 1)); + } + fwrite($FILE, pack('V', -2)); + + // Add to Data String(this will be written for RootEntry) + $sRes .= $raList[$i]->_data; + if ($raList[$i]->Size % $this->smallBlockSize) { + $sRes .= str_repeat("\x00", $this->smallBlockSize - ($raList[$i]->Size % $this->smallBlockSize)); + } + // Set for PPS + $raList[$i]->startBlock = $iSmBlk; + $iSmBlk += $iSmbCnt; + } + } + } + $iSbCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE); + if ($iSmBlk % $iSbCnt) { + $iB = $iSbCnt - ($iSmBlk % $iSbCnt); + for ($i = 0; $i < $iB; ++$i) { + fwrite($FILE, pack('V', -1)); + } + } + + return $sRes; + } + + /** + * Saves all the PPS's WKs. + * + * @param PPS[] $raList Reference to an array with all PPS's + */ + private function savePps(array &$raList): void + { + // Save each PPS WK + $iC = count($raList); + for ($i = 0; $i < $iC; ++$i) { + fwrite($this->fileHandle, $raList[$i]->getPpsWk()); + } + // Adjust for Block + $iCnt = count($raList); + $iBCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE; + if ($iCnt % $iBCnt) { + fwrite($this->fileHandle, str_repeat("\x00", ($iBCnt - ($iCnt % $iBCnt)) * OLE::OLE_PPS_SIZE)); + } + } + + /** + * Saving Big Block Depot. + */ + private function saveBbd(int $iSbdSize, int $iBsize, int $iPpsCnt): void + { + $FILE = $this->fileHandle; + // Calculate Basic Setting + $iBbCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE; + $i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE; + + $iBdExL = 0; + $iAll = $iBsize + $iPpsCnt + $iSbdSize; + $iAllW = $iAll; + $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0); + $iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0); + // Calculate BD count + if ($iBdCnt > $i1stBdL) { + while (1) { + ++$iBdExL; + ++$iAllW; + $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0); + $iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0); + if ($iBdCnt <= ($iBdExL * $iBbCnt + $i1stBdL)) { + break; + } + } + } + + // Making BD + // Set for SBD + if ($iSbdSize > 0) { + for ($i = 0; $i < ($iSbdSize - 1); ++$i) { + fwrite($FILE, pack('V', $i + 1)); + } + fwrite($FILE, pack('V', -2)); + } + // Set for B + for ($i = 0; $i < ($iBsize - 1); ++$i) { + fwrite($FILE, pack('V', $i + $iSbdSize + 1)); + } + fwrite($FILE, pack('V', -2)); + + // Set for PPS + for ($i = 0; $i < ($iPpsCnt - 1); ++$i) { + fwrite($FILE, pack('V', $i + $iSbdSize + $iBsize + 1)); + } + fwrite($FILE, pack('V', -2)); + // Set for BBD itself ( 0xFFFFFFFD : BBD) + for ($i = 0; $i < $iBdCnt; ++$i) { + fwrite($FILE, pack('V', 0xFFFFFFFD)); + } + // Set for ExtraBDList + for ($i = 0; $i < $iBdExL; ++$i) { + fwrite($FILE, pack('V', 0xFFFFFFFC)); + } + // Adjust for Block + if (($iAllW + $iBdCnt) % $iBbCnt) { + $iBlock = ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt)); + for ($i = 0; $i < $iBlock; ++$i) { + fwrite($FILE, pack('V', -1)); + } + } + // Extra BDList + if ($iBdCnt > $i1stBdL) { + $iN = 0; + $iNb = 0; + for ($i = $i1stBdL; $i < $iBdCnt; $i++, ++$iN) { + if ($iN >= ($iBbCnt - 1)) { + $iN = 0; + ++$iNb; + fwrite($FILE, pack('V', $iAll + $iBdCnt + $iNb)); + } + fwrite($FILE, pack('V', $iBsize + $iSbdSize + $iPpsCnt + $i)); + } + if (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)) { + $iB = ($iBbCnt - 1) - (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)); + for ($i = 0; $i < $iB; ++$i) { + fwrite($FILE, pack('V', -1)); + } + } + fwrite($FILE, pack('V', -2)); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLERead.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLERead.php new file mode 100644 index 0000000..5d38e95 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/OLERead.php @@ -0,0 +1,314 @@ +data = (string) file_get_contents($filename, false, null, 0, 8); + + // Check OLE identifier + $identifierOle = pack('CCCCCCCC', 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1); + if ($this->data != $identifierOle) { + throw new ReaderException('The filename ' . $filename . ' is not recognised as an OLE file'); + } + + // Get the file data + $this->data = (string) file_get_contents($filename); + + // Total number of sectors used for the SAT + $this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS); + + // SecID of the first sector of the directory stream + $this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS); + + // SecID of the first sector of the SSAT (or -2 if not extant) + $this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS); + + // SecID of the first sector of the MSAT (or -2 if no additional sectors are used) + $this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS); + + // Total number of sectors used by MSAT + $this->numExtensionBlocks = self::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS); + + $bigBlockDepotBlocks = []; + $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS; + + $bbdBlocks = $this->numBigBlockDepotBlocks; + + if ($this->numExtensionBlocks !== 0) { + $bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4; + } + + for ($i = 0; $i < $bbdBlocks; ++$i) { + $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); + $pos += 4; + } + + for ($j = 0; $j < $this->numExtensionBlocks; ++$j) { + $pos = ($this->extensionBlock + 1) * self::BIG_BLOCK_SIZE; + $blocksToRead = min($this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1); + + for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; ++$i) { + $bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); + $pos += 4; + } + + $bbdBlocks += $blocksToRead; + if ($bbdBlocks < $this->numBigBlockDepotBlocks) { + $this->extensionBlock = self::getInt4d($this->data, $pos); + } + } + + $pos = 0; + $this->bigBlockChain = ''; + $bbs = self::BIG_BLOCK_SIZE / 4; + for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) { + $pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE; + + $this->bigBlockChain .= substr($this->data, $pos, 4 * $bbs); + $pos += 4 * $bbs; + } + + $sbdBlock = $this->sbdStartBlock; + $this->smallBlockChain = ''; + while ($sbdBlock != -2) { + $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; + + $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); + $pos += 4 * $bbs; + + $sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4); + } + + // read the directory stream + $block = $this->rootStartBlock; + $this->entry = $this->readData($block); + + $this->readPropertySets(); + } + + /** + * Extract binary stream data. + */ + public function getStream(?int $stream): ?string + { + if ($stream === null) { + return null; + } + + $streamData = ''; + + if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) { + /** @var int */ + $temp = $this->props[$this->rootentry]['startBlock']; + $rootdata = $this->readData($temp); + + /** @var int */ + $block = $this->props[$stream]['startBlock']; + + while ($block != -2) { + $pos = $block * self::SMALL_BLOCK_SIZE; + $streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); + + $block = self::getInt4d($this->smallBlockChain, $block * 4); + } + + return $streamData; + } + /** @var int */ + $temp = $this->props[$stream]['size']; + $numBlocks = $temp / self::BIG_BLOCK_SIZE; + if ($temp % self::BIG_BLOCK_SIZE != 0) { + ++$numBlocks; + } + + if ($numBlocks == 0) { + return ''; + } + + /** @var int */ + $block = $this->props[$stream]['startBlock']; + + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block * 4); + } + + return $streamData; + } + + /** + * Read a standard stream (by joining sectors using information from SAT). + * + * @param int $block Sector ID where the stream starts + * + * @return string Data for standard stream + */ + private function readData(int $block): string + { + $data = ''; + + while ($block != -2) { + $pos = ($block + 1) * self::BIG_BLOCK_SIZE; + $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); + $block = self::getInt4d($this->bigBlockChain, $block * 4); + } + + return $data; + } + + /** + * Read entries in the directory stream. + */ + private function readPropertySets(): void + { + $offset = 0; + + // loop through entries, each entry is 128 bytes + $entryLen = strlen($this->entry); + while ($offset < $entryLen) { + // entry data (128 bytes) + $d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE); + + // size in bytes of name + $nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8); + + // type of entry + $type = ord($d[self::TYPE_POS]); + + // sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook) + // sectorID of first sector of the short-stream container stream, if this entry is root entry + $startBlock = self::getInt4d($d, self::START_BLOCK_POS); + + $size = self::getInt4d($d, self::SIZE_POS); + + $name = str_replace("\x00", '', substr($d, 0, $nameSize)); + + $this->props[] = [ + 'name' => $name, + 'type' => $type, + 'startBlock' => $startBlock, + 'size' => $size, + ]; + + // tmp helper to simplify checks + $upName = strtoupper($name); + + // Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook) + if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) { + $this->wrkbook = count($this->props) - 1; + } elseif ($upName === 'ROOT ENTRY' || $upName === 'R') { + // Root entry + $this->rootentry = count($this->props) - 1; + } + + // Summary information + if ($name == chr(5) . 'SummaryInformation') { + $this->summaryInformation = count($this->props) - 1; + } + + // Additional Document Summary information + if ($name == chr(5) . 'DocumentSummaryInformation') { + $this->documentSummaryInformation = count($this->props) - 1; + } + + $offset += self::PROPERTY_STORAGE_BLOCK_SIZE; + } + } + + /** + * Read 4 bytes of data at specified position. + */ + private static function getInt4d(string $data, int $pos): int + { + if ($pos < 0) { + // Invalid position + throw new ReaderException('Parameter pos=' . $pos . ' is invalid.'); + } + + $len = strlen($data); + if ($len < $pos + 4) { + $data .= str_repeat("\0", $pos + 4 - $len); + } + + // FIX: represent numbers correctly on 64-bit system + // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 + // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems + $_or_24 = ord($data[$pos + 3]); + if ($_or_24 >= 128) { + // negative number + $_ord_24 = -abs((256 - $_or_24) << 24); + } else { + $_ord_24 = ($_or_24 & 127) << 24; + } + + return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/PasswordHasher.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/PasswordHasher.php new file mode 100644 index 0000000..4d1103a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/PasswordHasher.php @@ -0,0 +1,106 @@ + 'md2', + Protection::ALGORITHM_MD4 => 'md4', + Protection::ALGORITHM_MD5 => 'md5', + Protection::ALGORITHM_SHA_1 => 'sha1', + Protection::ALGORITHM_SHA_256 => 'sha256', + Protection::ALGORITHM_SHA_384 => 'sha384', + Protection::ALGORITHM_SHA_512 => 'sha512', + Protection::ALGORITHM_RIPEMD_128 => 'ripemd128', + Protection::ALGORITHM_RIPEMD_160 => 'ripemd160', + Protection::ALGORITHM_WHIRLPOOL => 'whirlpool', + ]; + + if (array_key_exists($algorithmName, $mapping)) { + return $mapping[$algorithmName]; + } + + throw new SpException('Unsupported password algorithm: ' . $algorithmName); + } + + /** + * Create a password hash from a given string. + * + * This method is based on the spec at: + * https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/[MS-OFFCRYPTO].pdf + * 2.3.7.1 Binary Document Password Verifier Derivation Method 1 + * + * It replaces a method based on the algorithm provided by + * Daniel Rentz of OpenOffice and the PEAR package + * Spreadsheet_Excel_Writer by Xavier Noguer . + * + * @param string $password Password to hash + */ + private static function defaultHashPassword(string $password): string + { + $verifier = 0; + $pwlen = strlen($password); + $passwordArray = pack('c', $pwlen) . $password; + for ($i = $pwlen; $i >= 0; --$i) { + $intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1; + $intermediate2 = 2 * $verifier; + $intermediate2 = $intermediate2 & 0x7FFF; + $intermediate3 = $intermediate1 | $intermediate2; + $verifier = $intermediate3 ^ ord($passwordArray[$i]); + } + $verifier ^= 0xCE4B; + + return strtoupper(dechex($verifier)); + } + + /** + * Create a password hash from a given string by a specific algorithm. + * + * 2.4.2.4 ISO Write Protection Method + * + * @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f + * + * @param string $password Password to hash + * @param string $algorithm Hash algorithm used to compute the password hash value + * @param string $salt Pseudorandom base64-encoded string + * @param int $spinCount Number of times to iterate on a hash of a password + * + * @return string Hashed password + */ + public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string + { + if (strlen($password) > self::MAX_PASSWORD_LENGTH) { + throw new SpException('Password exceeds ' . self::MAX_PASSWORD_LENGTH . ' characters'); + } + $phpAlgorithm = self::getAlgorithm($algorithm); + if (!$phpAlgorithm) { + return self::defaultHashPassword($password); + } + + $saltValue = base64_decode($salt); + $encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + + $hashValue = hash($phpAlgorithm, $saltValue . $encodedPassword, true); + for ($i = 0; $i < $spinCount; ++$i) { + $hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true); + } + + return base64_encode($hashValue); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/StringHelper.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/StringHelper.php new file mode 100644 index 0000000..f6cb4f9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/StringHelper.php @@ -0,0 +1,739 @@ + "\x00", + "\x1B 1" => "\x01", + "\x1B 2" => "\x02", + "\x1B 3" => "\x03", + "\x1B 4" => "\x04", + "\x1B 5" => "\x05", + "\x1B 6" => "\x06", + "\x1B 7" => "\x07", + "\x1B 8" => "\x08", + "\x1B 9" => "\x09", + "\x1B :" => "\x0a", + "\x1B ;" => "\x0b", + "\x1B <" => "\x0c", + "\x1B =" => "\x0d", + "\x1B >" => "\x0e", + "\x1B ?" => "\x0f", + "\x1B!0" => "\x10", + "\x1B!1" => "\x11", + "\x1B!2" => "\x12", + "\x1B!3" => "\x13", + "\x1B!4" => "\x14", + "\x1B!5" => "\x15", + "\x1B!6" => "\x16", + "\x1B!7" => "\x17", + "\x1B!8" => "\x18", + "\x1B!9" => "\x19", + "\x1B!:" => "\x1a", + "\x1B!;" => "\x1b", + "\x1B!<" => "\x1c", + "\x1B!=" => "\x1d", + "\x1B!>" => "\x1e", + "\x1B!?" => "\x1f", + "\x1B'?" => "\x7f", + "\x1B(0" => '€', // 128 in CP1252 + "\x1B(2" => '‚', // 130 in CP1252 + "\x1B(3" => 'ƒ', // 131 in CP1252 + "\x1B(4" => '„', // 132 in CP1252 + "\x1B(5" => '…', // 133 in CP1252 + "\x1B(6" => '†', // 134 in CP1252 + "\x1B(7" => '‡', // 135 in CP1252 + "\x1B(8" => 'ˆ', // 136 in CP1252 + "\x1B(9" => '‰', // 137 in CP1252 + "\x1B(:" => 'Š', // 138 in CP1252 + "\x1B(;" => '‹', // 139 in CP1252 + "\x1BNj" => 'Œ', // 140 in CP1252 + "\x1B(>" => 'Ž', // 142 in CP1252 + "\x1B)1" => '‘', // 145 in CP1252 + "\x1B)2" => '’', // 146 in CP1252 + "\x1B)3" => '“', // 147 in CP1252 + "\x1B)4" => '”', // 148 in CP1252 + "\x1B)5" => '•', // 149 in CP1252 + "\x1B)6" => '–', // 150 in CP1252 + "\x1B)7" => '—', // 151 in CP1252 + "\x1B)8" => '˜', // 152 in CP1252 + "\x1B)9" => '™', // 153 in CP1252 + "\x1B):" => 'š', // 154 in CP1252 + "\x1B);" => '›', // 155 in CP1252 + "\x1BNz" => 'œ', // 156 in CP1252 + "\x1B)>" => 'ž', // 158 in CP1252 + "\x1B)?" => 'Ÿ', // 159 in CP1252 + "\x1B*0" => ' ', // 160 in CP1252 + "\x1BN!" => '¡', // 161 in CP1252 + "\x1BN\"" => '¢', // 162 in CP1252 + "\x1BN#" => '£', // 163 in CP1252 + "\x1BN(" => '¤', // 164 in CP1252 + "\x1BN%" => '¥', // 165 in CP1252 + "\x1B*6" => '¦', // 166 in CP1252 + "\x1BN'" => '§', // 167 in CP1252 + "\x1BNH " => '¨', // 168 in CP1252 + "\x1BNS" => '©', // 169 in CP1252 + "\x1BNc" => 'ª', // 170 in CP1252 + "\x1BN+" => '«', // 171 in CP1252 + "\x1B*<" => '¬', // 172 in CP1252 + "\x1B*=" => '­', // 173 in CP1252 + "\x1BNR" => '®', // 174 in CP1252 + "\x1B*?" => '¯', // 175 in CP1252 + "\x1BN0" => '°', // 176 in CP1252 + "\x1BN1" => '±', // 177 in CP1252 + "\x1BN2" => '²', // 178 in CP1252 + "\x1BN3" => '³', // 179 in CP1252 + "\x1BNB " => '´', // 180 in CP1252 + "\x1BN5" => 'µ', // 181 in CP1252 + "\x1BN6" => '¶', // 182 in CP1252 + "\x1BN7" => '·', // 183 in CP1252 + "\x1B+8" => '¸', // 184 in CP1252 + "\x1BNQ" => '¹', // 185 in CP1252 + "\x1BNk" => 'º', // 186 in CP1252 + "\x1BN;" => '»', // 187 in CP1252 + "\x1BN<" => '¼', // 188 in CP1252 + "\x1BN=" => '½', // 189 in CP1252 + "\x1BN>" => '¾', // 190 in CP1252 + "\x1BN?" => '¿', // 191 in CP1252 + "\x1BNAA" => 'À', // 192 in CP1252 + "\x1BNBA" => 'Á', // 193 in CP1252 + "\x1BNCA" => 'Â', // 194 in CP1252 + "\x1BNDA" => 'Ã', // 195 in CP1252 + "\x1BNHA" => 'Ä', // 196 in CP1252 + "\x1BNJA" => 'Å', // 197 in CP1252 + "\x1BNa" => 'Æ', // 198 in CP1252 + "\x1BNKC" => 'Ç', // 199 in CP1252 + "\x1BNAE" => 'È', // 200 in CP1252 + "\x1BNBE" => 'É', // 201 in CP1252 + "\x1BNCE" => 'Ê', // 202 in CP1252 + "\x1BNHE" => 'Ë', // 203 in CP1252 + "\x1BNAI" => 'Ì', // 204 in CP1252 + "\x1BNBI" => 'Í', // 205 in CP1252 + "\x1BNCI" => 'Î', // 206 in CP1252 + "\x1BNHI" => 'Ï', // 207 in CP1252 + "\x1BNb" => 'Ð', // 208 in CP1252 + "\x1BNDN" => 'Ñ', // 209 in CP1252 + "\x1BNAO" => 'Ò', // 210 in CP1252 + "\x1BNBO" => 'Ó', // 211 in CP1252 + "\x1BNCO" => 'Ô', // 212 in CP1252 + "\x1BNDO" => 'Õ', // 213 in CP1252 + "\x1BNHO" => 'Ö', // 214 in CP1252 + "\x1B-7" => '×', // 215 in CP1252 + "\x1BNi" => 'Ø', // 216 in CP1252 + "\x1BNAU" => 'Ù', // 217 in CP1252 + "\x1BNBU" => 'Ú', // 218 in CP1252 + "\x1BNCU" => 'Û', // 219 in CP1252 + "\x1BNHU" => 'Ü', // 220 in CP1252 + "\x1B-=" => 'Ý', // 221 in CP1252 + "\x1BNl" => 'Þ', // 222 in CP1252 + "\x1BN{" => 'ß', // 223 in CP1252 + "\x1BNAa" => 'à', // 224 in CP1252 + "\x1BNBa" => 'á', // 225 in CP1252 + "\x1BNCa" => 'â', // 226 in CP1252 + "\x1BNDa" => 'ã', // 227 in CP1252 + "\x1BNHa" => 'ä', // 228 in CP1252 + "\x1BNJa" => 'å', // 229 in CP1252 + "\x1BNq" => 'æ', // 230 in CP1252 + "\x1BNKc" => 'ç', // 231 in CP1252 + "\x1BNAe" => 'è', // 232 in CP1252 + "\x1BNBe" => 'é', // 233 in CP1252 + "\x1BNCe" => 'ê', // 234 in CP1252 + "\x1BNHe" => 'ë', // 235 in CP1252 + "\x1BNAi" => 'ì', // 236 in CP1252 + "\x1BNBi" => 'í', // 237 in CP1252 + "\x1BNCi" => 'î', // 238 in CP1252 + "\x1BNHi" => 'ï', // 239 in CP1252 + "\x1BNs" => 'ð', // 240 in CP1252 + "\x1BNDn" => 'ñ', // 241 in CP1252 + "\x1BNAo" => 'ò', // 242 in CP1252 + "\x1BNBo" => 'ó', // 243 in CP1252 + "\x1BNCo" => 'ô', // 244 in CP1252 + "\x1BNDo" => 'õ', // 245 in CP1252 + "\x1BNHo" => 'ö', // 246 in CP1252 + "\x1B/7" => '÷', // 247 in CP1252 + "\x1BNy" => 'ø', // 248 in CP1252 + "\x1BNAu" => 'ù', // 249 in CP1252 + "\x1BNBu" => 'ú', // 250 in CP1252 + "\x1BNCu" => 'û', // 251 in CP1252 + "\x1BNHu" => 'ü', // 252 in CP1252 + "\x1B/=" => 'ý', // 253 in CP1252 + "\x1BN|" => 'þ', // 254 in CP1252 + "\x1BNHy" => 'ÿ', // 255 in CP1252 + ]; + + /** + * Decimal separator. + */ + private static ?string $decimalSeparator = null; + + /** + * Thousands separator. + */ + private static ?string $thousandsSeparator = null; + + /** + * Currency code. + */ + private static ?string $currencyCode = null; + + /** + * Is iconv extension available? + */ + private static ?bool $isIconvEnabled = null; + + /** + * iconv options. + */ + private static string $iconvOptions = '//IGNORE//TRANSLIT'; + + /** + * Get whether iconv extension is available. + */ + public static function getIsIconvEnabled(): bool + { + if (isset(self::$isIconvEnabled)) { + return self::$isIconvEnabled; + } + + // Assume no problems with iconv + self::$isIconvEnabled = true; + + // Fail if iconv doesn't exist + if (!function_exists('iconv')) { + self::$isIconvEnabled = false; + } elseif (!@iconv('UTF-8', 'UTF-16LE', 'x')) { + // Sometimes iconv is not working, and e.g. iconv('UTF-8', 'UTF-16LE', 'x') just returns false, + self::$isIconvEnabled = false; + } elseif (defined('PHP_OS') && @stristr(PHP_OS, 'AIX') && defined('ICONV_IMPL') && (@strcasecmp(ICONV_IMPL, 'unknown') == 0) && defined('ICONV_VERSION') && (@strcasecmp(ICONV_VERSION, 'unknown') == 0)) { + // CUSTOM: IBM AIX iconv() does not work + self::$isIconvEnabled = false; + } + + // Deactivate iconv default options if they fail (as seen on IMB i) + if (self::$isIconvEnabled && !@iconv('UTF-8', 'UTF-16LE' . self::$iconvOptions, 'x')) { + self::$iconvOptions = ''; + } + + return self::$isIconvEnabled; + } + + /** + * Convert from OpenXML escaped control character to PHP control character. + * + * Excel 2007 team: + * ---------------- + * That's correct, control characters are stored directly in the shared-strings table. + * We do encode characters that cannot be represented in XML using the following escape sequence: + * _xHHHH_ where H represents a hexadecimal character in the character's value... + * So you could end up with something like _x0008_ in a string (either in a cell value () + * element or in the shared string element. + * + * @param string $textValue Value to unescape + */ + public static function controlCharacterOOXML2PHP(string $textValue): string + { + return str_replace(self::CONTROL_CHARACTERS_VALUES, self::CONTROL_CHARACTERS_KEYS, $textValue); + } + + /** + * Convert from PHP control character to OpenXML escaped control character. + * + * Excel 2007 team: + * ---------------- + * That's correct, control characters are stored directly in the shared-strings table. + * We do encode characters that cannot be represented in XML using the following escape sequence: + * _xHHHH_ where H represents a hexadecimal character in the character's value... + * So you could end up with something like _x0008_ in a string (either in a cell value () + * element or in the shared string element. + * + * @param string $textValue Value to escape + */ + public static function controlCharacterPHP2OOXML(string $textValue): string + { + return str_replace(self::CONTROL_CHARACTERS_KEYS, self::CONTROL_CHARACTERS_VALUES, $textValue); + } + + /** + * Try to sanitize UTF8, replacing invalid sequences with Unicode substitution characters. + */ + public static function sanitizeUTF8(string $textValue): string + { + $textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue); + $subst = mb_substitute_character(); // default is question mark + mb_substitute_character(65533); // Unicode substitution character + $returnValue = (string) mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); + mb_substitute_character($subst); + + return $returnValue; + } + + /** + * Check if a string contains UTF8 data. + */ + public static function isUTF8(string $textValue): bool + { + return $textValue === self::sanitizeUTF8($textValue); + } + + /** + * Formats a numeric value as a string for output in various output writers forcing + * point as decimal separator in case locale is other than English. + */ + public static function formatNumber(float|int|string|null $numericValue): string + { + if (is_float($numericValue)) { + return str_replace(',', '.', (string) $numericValue); + } + + return (string) $numericValue; + } + + /** + * Converts a UTF-8 string into BIFF8 Unicode string data (8-bit string length) + * Writes the string using uncompressed notation, no rich text, no Asian phonetics + * If mbstring extension is not available, ASCII is assumed, and compressed notation is used + * although this will give wrong results for non-ASCII strings + * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. + * + * @param string $textValue UTF-8 encoded string + * @param array $arrcRuns Details of rich text runs in $value + */ + public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string + { + // character count + $ln = self::countCharacters($textValue, 'UTF-8'); + // option flags + if (empty($arrcRuns)) { + $data = pack('CC', $ln, 0x0001); + // characters + $data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8'); + } else { + $data = pack('vC', $ln, 0x09); + $data .= pack('v', count($arrcRuns)); + // characters + $data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8'); + foreach ($arrcRuns as $cRun) { + $data .= pack('v', $cRun['strlen']); + $data .= pack('v', $cRun['fontidx']); + } + } + + return $data; + } + + /** + * Converts a UTF-8 string into BIFF8 Unicode string data (16-bit string length) + * Writes the string using uncompressed notation, no rich text, no Asian phonetics + * If mbstring extension is not available, ASCII is assumed, and compressed notation is used + * although this will give wrong results for non-ASCII strings + * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. + * + * @param string $textValue UTF-8 encoded string + */ + public static function UTF8toBIFF8UnicodeLong(string $textValue): string + { + // characters + $chars = self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8'); + $ln = (int) (strlen($chars) / 2); // N.B. - strlen, not mb_strlen issue #642 + + return pack('vC', $ln, 0x0001) . $chars; + } + + /** + * Convert string from one encoding to another. + * + * @param string $to Encoding to convert to, e.g. 'UTF-8' + * @param string $from Encoding to convert from, e.g. 'UTF-16LE' + */ + public static function convertEncoding(string $textValue, string $to, string $from): string + { + if (static::getIsIconvEnabled()) { + $result = iconv($from, $to . self::$iconvOptions, $textValue); + if (false !== $result) { + return $result; + } + } + + return (string) mb_convert_encoding($textValue, $to, $from); + } + + /** + * Get character count. + * + * @param string $encoding Encoding + * + * @return int Character count + */ + public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int + { + return mb_strlen($textValue, $encoding); + } + + /** + * Get character count using mb_strwidth rather than mb_strlen. + * + * @param string $encoding Encoding + * + * @return int Character count + */ + public static function countCharactersDbcs(string $textValue, string $encoding = 'UTF-8'): int + { + return mb_strwidth($textValue, $encoding); + } + + /** + * Get a substring of a UTF-8 encoded string. + * + * @param string $textValue UTF-8 encoded string + * @param int $offset Start offset + * @param ?int $length Maximum number of characters in substring + */ + public static function substring(string $textValue, int $offset, ?int $length = 0): string + { + return mb_substr($textValue, $offset, $length, 'UTF-8'); + } + + /** + * Convert a UTF-8 encoded string to upper case. + * + * @param string $textValue UTF-8 encoded string + */ + public static function strToUpper(string $textValue): string + { + return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8'); + } + + /** + * Convert a UTF-8 encoded string to lower case. + * + * @param string $textValue UTF-8 encoded string + */ + public static function strToLower(string $textValue): string + { + return mb_convert_case($textValue, MB_CASE_LOWER, 'UTF-8'); + } + + /** + * Convert a UTF-8 encoded string to title/proper case + * (uppercase every first character in each word, lower case all other characters). + * + * @param string $textValue UTF-8 encoded string + */ + public static function strToTitle(string $textValue): string + { + return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8'); + } + + public static function mbIsUpper(string $character): bool + { + return mb_strtolower($character, 'UTF-8') !== $character; + } + + /** + * Splits a UTF-8 string into an array of individual characters. + * + * @return string[] + */ + public static function mbStrSplit(string $string): array + { + // Split at all position not after the start: ^ + // and not before the end: $ + $split = Preg::split('/(? $v) { + $textValue = str_replace($k, $v, $textValue); + } + + return $textValue; + } + + /** + * Retrieve any leading numeric part of a string, or return the full string if no leading numeric + * (handles basic integer or float, but not exponent or non decimal). + * + * @return float|string string or only the leading numeric part of the string + */ + public static function testStringAsNumeric(string $textValue): float|string + { + if (is_numeric($textValue)) { + return $textValue; + } + $v = (float) $textValue; + + return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue; + } + + public static function strlenAllowNull(?string $string): int + { + return strlen("$string"); + } + + /** + * @param bool $convertBool If true, convert bool to locale-aware TRUE/FALSE rather than 1/null-string + * @param bool $lessFloatPrecision If true, floats will be converted to a more human-friendly but less computationally accurate value + */ + public static function convertToString(mixed $value, bool $throw = true, string $default = '', bool $convertBool = false, bool $lessFloatPrecision = false): string + { + if ($convertBool && is_bool($value)) { + return $value ? Calculation::getTRUE() : Calculation::getFALSE(); + } + if (is_float($value) && !$lessFloatPrecision) { + $string = (string) $value; + // look out for scientific notation + if (!Preg::isMatch('/[^-+0-9.]/', $string)) { + $minus = $value < 0 ? '-' : ''; + $positive = abs($value); + $floor = floor($positive); + $oldFrac = (string) ($positive - $floor); + $frac = Preg::replace('/^0[.](\d+)$/', '$1', $oldFrac); + if ($frac !== $oldFrac) { + return "$minus$floor.$frac"; + } + } + + return $string; + } + if ($value === null || is_scalar($value) || $value instanceof Stringable) { + return (string) $value; + } + + if ($throw) { + throw new SpreadsheetException('Unable to convert to string'); + } + + return $default; + } + + /** + * Assist with POST items when samples are run in browser. + * Never run as part of unit tests, which are command line. + * + * @codeCoverageIgnore + */ + public static function convertPostToString(string $index, string $default = ''): string + { + if (isset($_POST[$index])) { + return htmlentities(self::convertToString($_POST[$index], false, $default)); + } + + return $default; + } + + /** + * Php introduced str_increment with Php8.3, + * but didn't issue deprecation notices till 8.5. + * + * @codeCoverageIgnore + */ + public static function stringIncrement(string &$str): string + { + if (function_exists('str_increment')) { + $str = str_increment($str); // @phpstan-ignore-line + } else { + ++$str; // @phpstan-ignore-line + } + + return $str; // @phpstan-ignore-line + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/TimeZone.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/TimeZone.php new file mode 100644 index 0000000..f6e8500 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/TimeZone.php @@ -0,0 +1,75 @@ +setTimeZone(new DateTimeZone($timezoneName)); + + return $dtobj->getOffset(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/BestFit.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/BestFit.php new file mode 100644 index 0000000..3a17205 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/BestFit.php @@ -0,0 +1,429 @@ +error; + } + + public function getBestFitType(): string + { + return $this->bestFitType; + } + + /** + * Return the Y-Value for a specified value of X. + * + * @param float $xValue X-Value + * + * @return float Y-Value + */ + abstract public function getValueOfYForX(float $xValue): float; + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + abstract public function getValueOfXForY(float $yValue): float; + + /** + * Return the original set of X-Values. + * + * @return float[] X-Values + */ + public function getXValues(): array + { + return $this->xValues; + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + */ + abstract public function getEquation(int $dp = 0): string; + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getSlope(int $dp = 0): float + { + if ($dp != 0) { + return round($this->slope, $dp); + } + + return $this->slope; + } + + /** + * Return the standard error of the Slope. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getSlopeSE(int $dp = 0): float + { + if ($dp != 0) { + return round($this->slopeSE, $dp); + } + + return $this->slopeSE; + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getIntersect(int $dp = 0): float + { + if ($dp != 0) { + return round($this->intersect, $dp); + } + + return $this->intersect; + } + + /** + * Return the standard error of the Intersect. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getIntersectSE(int $dp = 0): float + { + if ($dp != 0) { + return round($this->intersectSE, $dp); + } + + return $this->intersectSE; + } + + /** + * Return the goodness of fit for this regression. + * + * @param int $dp Number of places of decimal precision to return + */ + public function getGoodnessOfFit(int $dp = 0): float + { + if ($dp != 0) { + return round($this->goodnessOfFit, $dp); + } + + return $this->goodnessOfFit; + } + + /** + * Return the goodness of fit for this regression. + * + * @param int $dp Number of places of decimal precision to return + */ + public function getGoodnessOfFitPercent(int $dp = 0): float + { + if ($dp != 0) { + return round($this->goodnessOfFit * 100, $dp); + } + + return $this->goodnessOfFit * 100; + } + + /** + * Return the standard deviation of the residuals for this regression. + * + * @param int $dp Number of places of decimal precision to return + */ + public function getStdevOfResiduals(int $dp = 0): float + { + if ($dp != 0) { + return round($this->stdevOfResiduals, $dp); + } + + return $this->stdevOfResiduals; + } + + /** + * @param int $dp Number of places of decimal precision to return + */ + public function getSSRegression(int $dp = 0): float + { + if ($dp != 0) { + return round($this->SSRegression, $dp); + } + + return $this->SSRegression; + } + + /** + * @param int $dp Number of places of decimal precision to return + */ + public function getSSResiduals(int $dp = 0): float + { + if ($dp != 0) { + return round($this->SSResiduals, $dp); + } + + return $this->SSResiduals; + } + + /** + * @param int $dp Number of places of decimal precision to return + */ + public function getDFResiduals(int $dp = 0): float + { + if ($dp != 0) { + return round($this->DFResiduals, $dp); + } + + return $this->DFResiduals; + } + + /** + * @param int $dp Number of places of decimal precision to return + */ + public function getF(int $dp = 0): float + { + if ($dp != 0) { + return round($this->f, $dp); + } + + return $this->f; + } + + /** + * @param int $dp Number of places of decimal precision to return + */ + public function getCovariance(int $dp = 0): float + { + if ($dp != 0) { + return round($this->covariance, $dp); + } + + return $this->covariance; + } + + /** + * @param int $dp Number of places of decimal precision to return + */ + public function getCorrelation(int $dp = 0): float + { + if ($dp != 0) { + return round($this->correlation, $dp); + } + + return $this->correlation; + } + + /** + * @return float[] + */ + public function getYBestFitValues(): array + { + return $this->yBestFitValues; + } + + protected function calculateGoodnessOfFit(float $sumX, float $sumY, float $sumX2, float $sumY2, float $sumXY, float $meanX, float $meanY, bool|int $const): void + { + $SSres = $SScov = $SStot = $SSsex = 0.0; + foreach ($this->xValues as $xKey => $xValue) { + $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); + + $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY); + if ($const === true) { + $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY); + } else { + $SStot += $this->yValues[$xKey] * $this->yValues[$xKey]; + } + $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY); + if ($const === true) { + $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX); + } else { + $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey]; + } + } + + $this->SSResiduals = $SSres; + $this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0); + + if ($this->DFResiduals == 0.0) { + $this->stdevOfResiduals = 0.0; + } else { + $this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals); + } + + if ($SStot == 0.0 || $SSres == $SStot) { + $this->goodnessOfFit = 1; + } else { + $this->goodnessOfFit = 1 - ($SSres / $SStot); + } + + $this->SSRegression = $this->goodnessOfFit * $SStot; + $this->covariance = $SScov / $this->valueCount; + $this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - $sumX ** 2) * ($this->valueCount * $sumY2 - $sumY ** 2)); + $this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex); + $this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2)); + if ($this->SSResiduals != 0.0) { + if ($this->DFResiduals == 0.0) { + $this->f = 0.0; + } else { + $this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals); + } + } else { + if ($this->DFResiduals == 0.0) { + $this->f = 0.0; + } else { + $this->f = $this->SSRegression / $this->DFResiduals; + } + } + } + + /** + * @param array $values + * + * @return float|int + */ + private function sumSquares(array $values) + { + return array_sum( + array_map( + fn ($value): float|int => $value ** 2, + $values + ) + ); + } + + /** + * @param float[] $yValues + * @param float[] $xValues + */ + protected function leastSquareFit(array $yValues, array $xValues, bool $const): void + { + // calculate sums + $sumValuesX = array_sum($xValues); + $sumValuesY = array_sum($yValues); + $meanValueX = $sumValuesX / $this->valueCount; + $meanValueY = $sumValuesY / $this->valueCount; + $sumSquaresX = $this->sumSquares($xValues); + $sumSquaresY = $this->sumSquares($yValues); + $mBase = $mDivisor = 0.0; + $xy_sum = 0.0; + for ($i = 0; $i < $this->valueCount; ++$i) { + $xy_sum += $xValues[$i] * $yValues[$i]; + + if ($const === true) { + $mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY); + $mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX); + } else { + $mBase += $xValues[$i] * $yValues[$i]; + $mDivisor += $xValues[$i] * $xValues[$i]; + } + } + + // calculate slope + $this->slope = $mBase / $mDivisor; + + // calculate intersect + $this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0; + + $this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const); + } + + /** + * Define the regression. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + public function __construct(array $yValues, array $xValues = []) + { + // Calculate number of points + $yValueCount = count($yValues); + $xValueCount = count($xValues); + + // Define X Values if necessary + if ($xValueCount === 0) { + $xValues = range(1, $yValueCount); + } elseif ($yValueCount !== $xValueCount) { + // Ensure both arrays of points are the same size + $this->error = true; + } + + $this->valueCount = $yValueCount; + $this->xValues = $xValues; + $this->yValues = $yValues; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php new file mode 100644 index 0000000..ed2d889 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php @@ -0,0 +1,108 @@ +getIntersect() * $this->getSlope() ** ($xValue - $this->xOffset); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY(float $yValue): float + { + return log(($yValue + $this->yOffset) / $this->getIntersect()) / log($this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getEquation(int $dp = 0): string + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' * ' . $slope . '^X'; + } + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getSlope(int $dp = 0): float + { + if ($dp != 0) { + return round(exp($this->slope), $dp); + } + + return exp($this->slope); + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getIntersect(int $dp = 0): float + { + if ($dp != 0) { + return round(exp($this->intersect), $dp); + } + + return exp($this->intersect); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function exponentialRegression(array $yValues, array $xValues, bool $const): void + { + $adjustedYValues = array_map( + fn ($value): float => ($value < 0.0) ? 0 - log(abs($value)) : log($value), + $yValues + ); + + $this->leastSquareFit($adjustedYValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + public function __construct(array $yValues, array $xValues = [], bool $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->exponentialRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php new file mode 100644 index 0000000..8a54080 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php @@ -0,0 +1,75 @@ +getIntersect() + $this->getSlope() * $xValue; + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY(float $yValue): float + { + return ($yValue - $this->getIntersect()) / $this->getSlope(); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getEquation(int $dp = 0): string + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' + ' . $slope . ' * X'; + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function linearRegression(array $yValues, array $xValues, bool $const): void + { + $this->leastSquareFit($yValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + public function __construct(array $yValues, array $xValues = [], bool $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->linearRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php new file mode 100644 index 0000000..3dec61b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php @@ -0,0 +1,80 @@ +getIntersect() + $this->getSlope() * log($xValue - $this->xOffset); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY(float $yValue): float + { + return exp(($yValue - $this->getIntersect()) / $this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getEquation(int $dp = 0): string + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)'; + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function logarithmicRegression(array $yValues, array $xValues, bool $const): void + { + $adjustedYValues = array_map( + fn ($value): float => ($value < 0.0) ? 0 - log(abs($value)) : log($value), + $yValues + ); + + $this->leastSquareFit($adjustedYValues, $xValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + public function __construct(array $yValues, array $xValues = [], bool $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->logarithmicRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php new file mode 100644 index 0000000..0fc6671 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -0,0 +1,220 @@ +slope is specified where an array is expected in several places. +// But it seems that it should always be float. +// This code is probably not exercised at all in unit tests. +// Private bool property $implemented is set to indicate +// whether this implementation is correct. +class PolynomialBestFit extends BestFit +{ + /** + * Algorithm type to use for best-fit + * (Name of this Trend class). + */ + protected string $bestFitType = 'polynomial'; + + /** + * Polynomial order. + */ + protected int $order = 0; + + private bool $implemented = false; + + /** + * Return the order of this polynomial. + */ + public function getOrder(): int + { + return $this->order; + } + + /** + * Return the Y-Value for a specified value of X. + * + * @param float $xValue X-Value + * + * @return float Y-Value + */ + public function getValueOfYForX(float $xValue): float + { + $retVal = $this->getIntersect(); + $slope = $this->getSlope(); + // Phpstan and Scrutinizer are both correct - getSlope returns float, not array. + // @phpstan-ignore-next-line + foreach ($slope as $key => $value) { + /** @var float $value */ + if ($value != 0.0) { + /** @var int $key */ + $retVal += $value * $xValue ** ($key + 1); + } + } + + return $retVal; + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY(float $yValue): float + { + return ($yValue - $this->getIntersect()) / $this->getSlope(); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getEquation(int $dp = 0): string + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + $equation = 'Y = ' . $intersect; + // Phpstan and Scrutinizer are both correct - getSlope returns float, not array. + // @phpstan-ignore-next-line + foreach ($slope as $key => $value) { + /** @var float|int $value */ + if ($value != 0.0) { + $equation .= ' + ' . $value . ' * X'; + /** @var int $key */ + if ($key > 0) { + $equation .= '^' . ($key + 1); + } + } + } + + return $equation; + } + + /** + * Return the Slope of the line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getSlope(int $dp = 0): float + { + if ($dp != 0) { + $coefficients = []; + //* @phpstan-ignore-next-line + foreach ($this->slope as $coefficient) { + /** @var float|int $coefficient */ + $coefficients[] = round($coefficient, $dp); + } + + // @phpstan-ignore-next-line + return $coefficients; + } + + return $this->slope; + } + + /** @return array */ + public function getCoefficients(int $dp = 0): array + { + // Phpstan and Scrutinizer are both correct - getSlope returns float, not array. + // @phpstan-ignore-next-line + return array_merge([$this->getIntersect($dp)], $this->getSlope($dp)); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param int $order Order of Polynomial for this regression + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function polynomialRegression(int $order, array $yValues, array $xValues): void + { + // calculate sums + $x_sum = array_sum($xValues); + $y_sum = array_sum($yValues); + $xx_sum = $xy_sum = $yy_sum = 0; + for ($i = 0; $i < $this->valueCount; ++$i) { + $xy_sum += $xValues[$i] * $yValues[$i]; + $xx_sum += $xValues[$i] * $xValues[$i]; + $yy_sum += $yValues[$i] * $yValues[$i]; + } + /* + * This routine uses logic from the PHP port of polyfit version 0.1 + * written by Michael Bommarito and Paul Meagher + * + * The function fits a polynomial function of order $order through + * a series of x-y data points using least squares. + * + */ + $A = []; + $B = []; + for ($i = 0; $i < $this->valueCount; ++$i) { + for ($j = 0; $j <= $order; ++$j) { + $A[$i][$j] = $xValues[$i] ** $j; + } + } + for ($i = 0; $i < $this->valueCount; ++$i) { + $B[$i] = [$yValues[$i]]; + } + $matrixA = new Matrix($A); + $matrixB = new Matrix($B); + $C = $matrixA->solve($matrixB); + + $coefficients = []; + for ($i = 0; $i < $C->rows; ++$i) { + $r = $C->getValue($i + 1, 1); // row and column are origin-1 + if (!is_numeric($r) || abs($r + 0) <= 10 ** (-9)) { + $r = 0; + } else { + $r += 0; + } + $coefficients[] = $r; + } + + $this->intersect = (float) array_shift($coefficients); + // Phpstan is correct + //* @phpstan-ignore-next-line + $this->slope = $coefficients; + + $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0); + foreach ($this->xValues as $xKey => $xValue) { + $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); + } + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param int $order Order of Polynomial for this regression + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + public function __construct(int $order, array $yValues, array $xValues = []) + { + if (!$this->implemented) { + throw new SpreadsheetException('Polynomial Best Fit not yet implemented'); + } + + parent::__construct($yValues, $xValues); + + if (!$this->error) { + if ($order < $this->valueCount) { + $this->bestFitType .= '_' . $order; + $this->order = $order; + $this->polynomialRegression($order, $yValues, $xValues); + if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) { + $this->error = true; + } + } else { + $this->error = true; + } + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php new file mode 100644 index 0000000..56b5a12 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php @@ -0,0 +1,98 @@ +getIntersect() * ($xValue - $this->xOffset) ** $this->getSlope(); + } + + /** + * Return the X-Value for a specified value of Y. + * + * @param float $yValue Y-Value + * + * @return float X-Value + */ + public function getValueOfXForY(float $yValue): float + { + return (($yValue + $this->yOffset) / $this->getIntersect()) ** (1 / $this->getSlope()); + } + + /** + * Return the Equation of the best-fit line. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getEquation(int $dp = 0): string + { + $slope = $this->getSlope($dp); + $intersect = $this->getIntersect($dp); + + return 'Y = ' . $intersect . ' * X^' . $slope; + } + + /** + * Return the Value of X where it intersects Y = 0. + * + * @param int $dp Number of places of decimal precision to display + */ + public function getIntersect(int $dp = 0): float + { + if ($dp != 0) { + return round(exp($this->intersect), $dp); + } + + return exp($this->intersect); + } + + /** + * Execute the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + private function powerRegression(array $yValues, array $xValues, bool $const): void + { + $adjustedYValues = array_map( + fn ($value): float => ($value < 0.0) ? 0 - log(abs($value)) : log($value), + $yValues + ); + $adjustedXValues = array_map( + fn ($value): float => ($value < 0.0) ? 0 - log(abs($value)) : log($value), + $xValues + ); + + $this->leastSquareFit($adjustedYValues, $adjustedXValues, $const); + } + + /** + * Define the regression and calculate the goodness of fit for a set of X and Y data values. + * + * @param float[] $yValues The set of Y-values for this regression + * @param float[] $xValues The set of X-values for this regression + */ + public function __construct(array $yValues, array $xValues = [], bool $const = true) + { + parent::__construct($yValues, $xValues); + + if (!$this->error) { + $this->powerRegression($yValues, $xValues, (bool) $const); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/Trend.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/Trend.php new file mode 100644 index 0000000..e44c669 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -0,0 +1,132 @@ +getGoodnessOfFit(); + } + if ($trendType !== self::TREND_BEST_FIT_NO_POLY) { + foreach (self::$trendTypePolynomialOrders as $trendMethod) { + $order = (int) substr($trendMethod, -1); + $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues); + if ($bestFit[$trendMethod]->getError()) { + unset($bestFit[$trendMethod]); + } else { + $bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit(); + } + } + } + // Determine which of our Trend lines is the best fit, and then we return the instance of that Trend class + arsort($bestFitValue); + $bestFitType = key($bestFitValue); + + return $bestFit[$bestFitType]; + default: + throw new SpreadsheetException("Unknown trend type $trendType"); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/XMLWriter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/XMLWriter.php new file mode 100644 index 0000000..17b1ee6 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/XMLWriter.php @@ -0,0 +1,101 @@ +openMemory(); + } else { + // Create temporary filename + if ($temporaryStorageFolder === null) { + $temporaryStorageFolder = File::sysGetTempDir(); + } + $this->tempFileName = (string) @tempnam($temporaryStorageFolder, 'xml'); + + // Open storage + if (empty($this->tempFileName) || $this->openUri($this->tempFileName) === false) { + // Fallback to memory... + $this->openMemory(); + if ($this->tempFileName != '') { + @unlink($this->tempFileName); + } + $this->tempFileName = ''; + } + } + + // Set default values + if (self::$debugEnabled) { + $this->setIndent(true); + } + } + + /** + * Destructor. + */ + public function __destruct() + { + // Unlink temporary files + // There is nothing reasonable to do if unlink fails. + if ($this->tempFileName != '') { + @unlink($this->tempFileName); + } + } + + /** @param mixed[] $data */ + public function __unserialize(array $data): void + { + $this->tempFileName = ''; + + throw new SpreadsheetException('Unserialize not permitted'); + } + + /** + * Get written data. + */ + public function getData(): string + { + if ($this->tempFileName == '') { + return $this->outputMemory(true); + } + $this->flush(); + + return file_get_contents($this->tempFileName) ?: ''; + } + + /** + * Wrapper method for writeRaw. + * + * @param null|string|string[] $rawTextData + */ + public function writeRawData($rawTextData): bool + { + if (is_array($rawTextData)) { + $rawTextData = implode("\n", $rawTextData); + } + + return $this->text($rawTextData ?? ''); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Xls.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Xls.php new file mode 100644 index 0000000..6e34a5d --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Shared/Xls.php @@ -0,0 +1,275 @@ +getParentOrThrow()->getDefaultStyle()->getFont(); + + $columnDimensions = $worksheet->getColumnDimensions(); + + // first find the true column width in pixels (uncollapsed and unhidden) + if (isset($columnDimensions[$col]) && $columnDimensions[$col]->getWidth() != -1) { + // then we have column dimension with explicit width + $columnDimension = $columnDimensions[$col]; + $width = $columnDimension->getWidth(); + $pixelWidth = Drawing::cellDimensionToPixels($width, $font); + } elseif ($worksheet->getDefaultColumnDimension()->getWidth() != -1) { + // then we have default column dimension with explicit width + $defaultColumnDimension = $worksheet->getDefaultColumnDimension(); + $width = $defaultColumnDimension->getWidth(); + $pixelWidth = Drawing::cellDimensionToPixels($width, $font); + } else { + // we don't even have any default column dimension. Width depends on default font + $pixelWidth = Font::getDefaultColumnWidthByFont($font, true); + } + + // now find the effective column width in pixels + if (isset($columnDimensions[$col]) && !$columnDimensions[$col]->getVisible()) { + $effectivePixelWidth = 0; + } else { + $effectivePixelWidth = $pixelWidth; + } + + return $effectivePixelWidth; + } + + /** + * Convert the height of a cell from user's units to pixels. By interpolation + * the relationship is: y = 4/3x. If the height hasn't been set by the user we + * use the default value. If the row is hidden we use a value of zero. + * + * @param Worksheet $worksheet The sheet + * @param int $row The row index (1-based) + * + * @return int The width in pixels + */ + public static function sizeRow(Worksheet $worksheet, int $row = 1): int + { + // default font of the workbook + $font = $worksheet->getParentOrThrow()->getDefaultStyle()->getFont(); + + $rowDimensions = $worksheet->getRowDimensions(); + + // first find the true row height in pixels (uncollapsed and unhidden) + if (isset($rowDimensions[$row]) && $rowDimensions[$row]->getRowHeight() != -1) { + // then we have a row dimension + $rowDimension = $rowDimensions[$row]; + $rowHeight = $rowDimension->getRowHeight(); + $pixelRowHeight = (int) ceil(4 * $rowHeight / 3); // here we assume Arial 10 + } elseif ($worksheet->getDefaultRowDimension()->getRowHeight() != -1) { + // then we have a default row dimension with explicit height + $defaultRowDimension = $worksheet->getDefaultRowDimension(); + $pixelRowHeight = $defaultRowDimension->getRowHeight(Dimension::UOM_PIXELS); + } else { + // we don't even have any default row dimension. Height depends on default font + $pointRowHeight = Font::getDefaultRowHeightByFont($font); + $pixelRowHeight = Font::fontSizeToPixels((int) $pointRowHeight); + } + + // now find the effective row height in pixels + if (isset($rowDimensions[$row]) && !$rowDimensions[$row]->getVisible()) { + $effectivePixelRowHeight = 0; + } else { + $effectivePixelRowHeight = $pixelRowHeight; + } + + return (int) $effectivePixelRowHeight; + } + + /** + * Get the horizontal distance in pixels between two anchors + * The distanceX is found as sum of all the spanning columns widths minus correction for the two offsets. + * + * @param float|int $startOffsetX Offset within start cell measured in 1/1024 of the cell width + * @param float|int $endOffsetX Offset within end cell measured in 1/1024 of the cell width + * + * @return int Horizontal measured in pixels + */ + public static function getDistanceX(Worksheet $worksheet, string $startColumn = 'A', float|int $startOffsetX = 0, string $endColumn = 'A', float|int $endOffsetX = 0): int + { + $distanceX = 0; + + // add the widths of the spanning columns + $startColumnIndex = Coordinate::columnIndexFromString($startColumn); + $endColumnIndex = Coordinate::columnIndexFromString($endColumn); + for ($i = $startColumnIndex; $i <= $endColumnIndex; ++$i) { + $distanceX += self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($i)); + } + + // correct for offsetX in startcell + $distanceX -= (int) floor(self::sizeCol($worksheet, $startColumn) * $startOffsetX / 1024); + + // correct for offsetX in endcell + $distanceX -= (int) floor(self::sizeCol($worksheet, $endColumn) * (1 - $endOffsetX / 1024)); + + return $distanceX; + } + + /** + * Get the vertical distance in pixels between two anchors + * The distanceY is found as sum of all the spanning rows minus two offsets. + * + * @param int $startRow (1-based) + * @param float|int $startOffsetY Offset within start cell measured in 1/256 of the cell height + * @param int $endRow (1-based) + * @param float|int $endOffsetY Offset within end cell measured in 1/256 of the cell height + * + * @return int Vertical distance measured in pixels + */ + public static function getDistanceY(Worksheet $worksheet, int $startRow = 1, float|int $startOffsetY = 0, int $endRow = 1, float|int $endOffsetY = 0): int + { + $distanceY = 0; + + // add the widths of the spanning rows + for ($row = $startRow; $row <= $endRow; ++$row) { + $distanceY += self::sizeRow($worksheet, $row); + } + + // correct for offsetX in startcell + $distanceY -= (int) floor(self::sizeRow($worksheet, $startRow) * $startOffsetY / 256); + + // correct for offsetX in endcell + $distanceY -= (int) floor(self::sizeRow($worksheet, $endRow) * (1 - $endOffsetY / 256)); + + return $distanceY; + } + + /** + * Convert 1-cell anchor coordinates to 2-cell anchor coordinates + * This function is ported from PEAR Spreadsheet_Writer_Excel with small modifications. + * + * Calculate the vertices that define the position of the image as required by + * the OBJ record. + * + * +------------+------------+ + * | A | B | + * +-----+------------+------------+ + * | |(x1,y1) | | + * | 1 |(A1)._______|______ | + * | | | | | + * | | | | | + * +-----+----| BITMAP |-----+ + * | | | | | + * | 2 | |______________. | + * | | | (B2)| + * | | | (x2,y2)| + * +---- +------------+------------+ + * + * Example of a bitmap that covers some of the area from cell A1 to cell B2. + * + * Based on the width and height of the bitmap we need to calculate 8 vars: + * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. + * The width and height of the cells are also variable and have to be taken into + * account. + * The values of $col_start and $row_start are passed in from the calling + * function. The values of $col_end and $row_end are calculated by subtracting + * the width and height of the bitmap from the width and height of the + * underlying cells. + * The vertices are expressed as a percentage of the underlying cell width as + * follows (rhs values are in pixels): + * + * x1 = X / W *1024 + * y1 = Y / H *256 + * x2 = (X-1) / W *1024 + * y2 = (Y-1) / H *256 + * + * Where: X is distance from the left side of the underlying cell + * Y is distance from the top of the underlying cell + * W is the width of the cell + * H is the height of the cell + * + * @param string $coordinates E.g. 'A1' + * @param int $offsetX Horizontal offset in pixels + * @param int $offsetY Vertical offset in pixels + * @param int $width Width in pixels + * @param int $height Height in pixels + * + * @return array{startCoordinates: string, startOffsetX: int, startOffsetY: int, endCoordinates: string, endOffsetX: int, endOffsetY: int} + */ + public static function oneAnchor2twoAnchor(Worksheet $worksheet, string $coordinates, int $offsetX, int $offsetY, int $width, int $height): ?array + { + [$col_start, $row] = Coordinate::indexesFromString($coordinates); + $row_start = $row - 1; + + $x1 = $offsetX; + $y1 = $offsetY; + + // Initialise end cell to the same as the start cell + $col_end = $col_start; // Col containing lower right corner of object + $row_end = $row_start; // Row containing bottom right corner of object + + // Zero the specified offset if greater than the cell dimensions + if ($x1 >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start))) { + $x1 = 0; + } + if ($y1 >= self::sizeRow($worksheet, $row_start + 1)) { + $y1 = 0; + } + + $width = $width + $x1 - 1; + $height = $height + $y1 - 1; + + // Subtract the underlying cell widths to find the end cell of the image + while ($width >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end))) { + $width -= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)); + ++$col_end; + } + + // Subtract the underlying cell heights to find the end cell of the image + while ($height >= self::sizeRow($worksheet, $row_end + 1)) { + $height -= self::sizeRow($worksheet, $row_end + 1); + ++$row_end; + } + + // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell + // with zero height or width. + if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) == 0) { + return null; + } + if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) == 0) { + return null; + } + if (self::sizeRow($worksheet, $row_start + 1) == 0) { + return null; + } + if (self::sizeRow($worksheet, $row_end + 1) == 0) { + return null; + } + + // Convert the pixel values to the percentage value expected by Excel + $x1 = $x1 / self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) * 1024; + $y1 = $y1 / self::sizeRow($worksheet, $row_start + 1) * 256; + $x2 = ($width + 1) / self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object + $y2 = ($height + 1) / self::sizeRow($worksheet, $row_end + 1) * 256; // Distance to bottom of object + + $startCoordinates = Coordinate::stringFromColumnIndex($col_start) . ($row_start + 1); + $endCoordinates = Coordinate::stringFromColumnIndex($col_end) . ($row_end + 1); + + return [ + 'startCoordinates' => $startCoordinates, + 'startOffsetX' => $x1, + 'startOffsetY' => $y1, + 'endCoordinates' => $endCoordinates, + 'endOffsetX' => $x2, + 'endOffsetY' => $y2, + ]; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Spreadsheet.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Spreadsheet.php new file mode 100644 index 0000000..42504ad --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Spreadsheet.php @@ -0,0 +1,1854 @@ +|string>>> + */ + private array $unparsedLoadedData = []; + + /** + * Controls visibility of the horizonal scroll bar in the application. + */ + private bool $showHorizontalScroll = true; + + /** + * Controls visibility of the horizonal scroll bar in the application. + */ + private bool $showVerticalScroll = true; + + /** + * Controls visibility of the sheet tabs in the application. + */ + private bool $showSheetTabs = true; + + /** + * Specifies a boolean value that indicates whether the workbook window + * is minimized. + */ + private bool $minimized = false; + + /** + * Specifies a boolean value that indicates whether to group dates + * when presenting the user with filtering options in the user + * interface. + */ + private bool $autoFilterDateGrouping = true; + + /** + * Specifies the index to the first sheet in the book view. + */ + private int $firstSheetIndex = 0; + + /** + * Specifies the visible status of the workbook. + */ + private string $visibility = self::VISIBILITY_VISIBLE; + + /** + * Specifies the ratio between the workbook tabs bar and the horizontal + * scroll bar. TabRatio is assumed to be out of 1000 of the horizontal + * window width. + */ + private int $tabRatio = 600; + + private Theme $theme; + + private ?IValueBinder $valueBinder = null; + + /** @var array */ + private array $fontCharsets = [ + 'B Nazanin' => SharedFont::CHARSET_ANSI_ARABIC, + ]; + + /** + * @param int $charset uses any value from Shared\Font, + * but defaults to ARABIC because that is the only known + * charset for which this declaration might be needed + */ + public function addFontCharset(string $fontName, int $charset = SharedFont::CHARSET_ANSI_ARABIC): void + { + $this->fontCharsets[$fontName] = $charset; + } + + public function getFontCharset(string $fontName): int + { + return $this->fontCharsets[$fontName] ?? -1; + } + + /** + * Return all fontCharsets. + * + * @return array + */ + public function getFontCharsets(): array + { + return $this->fontCharsets; + } + + public function getTheme(): Theme + { + return $this->theme; + } + + /** + * The workbook has macros ? + */ + public function hasMacros(): bool + { + return $this->hasMacros; + } + + /** + * Define if a workbook has macros. + * + * @param bool $hasMacros true|false + */ + public function setHasMacros(bool $hasMacros): void + { + $this->hasMacros = (bool) $hasMacros; + } + + /** + * Set the macros code. + */ + public function setMacrosCode(?string $macroCode): void + { + $this->macrosCode = $macroCode; + $this->setHasMacros($macroCode !== null); + } + + /** + * Return the macros code. + */ + public function getMacrosCode(): ?string + { + return $this->macrosCode; + } + + /** + * Set the macros certificate. + */ + public function setMacrosCertificate(?string $certificate): void + { + $this->macrosCertificate = $certificate; + } + + /** + * Is the project signed ? + * + * @return bool true|false + */ + public function hasMacrosCertificate(): bool + { + return $this->macrosCertificate !== null; + } + + /** + * Return the macros certificate. + */ + public function getMacrosCertificate(): ?string + { + return $this->macrosCertificate; + } + + /** + * Remove all macros, certificate from spreadsheet. + */ + public function discardMacros(): void + { + $this->hasMacros = false; + $this->macrosCode = null; + $this->macrosCertificate = null; + } + + /** + * set ribbon XML data. + */ + public function setRibbonXMLData(mixed $target, mixed $xmlData): void + { + if (is_string($target) && is_string($xmlData)) { + $this->ribbonXMLData = ['target' => $target, 'data' => $xmlData]; + } else { + $this->ribbonXMLData = null; + } + } + + /** + * retrieve ribbon XML Data. + * + * @return mixed[] + */ + public function getRibbonXMLData(string $what = 'all'): null|array|string //we need some constants here... + { + $returnData = null; + $what = strtolower($what); + switch ($what) { + case 'all': + $returnData = $this->ribbonXMLData; + + break; + case 'target': + case 'data': + if (is_array($this->ribbonXMLData)) { + $returnData = $this->ribbonXMLData[$what]; + } + + break; + } + + return $returnData; + } + + /** + * store binaries ribbon objects (pictures). + */ + public function setRibbonBinObjects(mixed $binObjectsNames, mixed $binObjectsData): void + { + if ($binObjectsNames !== null && $binObjectsData !== null) { + $this->ribbonBinObjects = ['names' => $binObjectsNames, 'data' => $binObjectsData]; + } else { + $this->ribbonBinObjects = null; + } + } + + /** + * List of unparsed loaded data for export to same format with better compatibility. + * It has to be minimized when the library start to support currently unparsed data. + * + * @internal + * + * @return mixed[] + */ + public function getUnparsedLoadedData(): array + { + return $this->unparsedLoadedData; + } + + /** + * List of unparsed loaded data for export to same format with better compatibility. + * It has to be minimized when the library start to support currently unparsed data. + * + * @internal + * + * @param array|string>>> $unparsedLoadedData + */ + public function setUnparsedLoadedData(array $unparsedLoadedData): void + { + $this->unparsedLoadedData = $unparsedLoadedData; + } + + /** + * retrieve Binaries Ribbon Objects. + * + * @return mixed[] + */ + public function getRibbonBinObjects(string $what = 'all'): ?array + { + $ReturnData = null; + $what = strtolower($what); + switch ($what) { + case 'all': + return $this->ribbonBinObjects; + case 'names': + case 'data': + if (is_array($this->ribbonBinObjects) && is_array($this->ribbonBinObjects[$what] ?? null)) { + $ReturnData = $this->ribbonBinObjects[$what]; + } + + break; + case 'types': + if ( + is_array($this->ribbonBinObjects) + && isset($this->ribbonBinObjects['data']) && is_array($this->ribbonBinObjects['data']) + ) { + $tmpTypes = array_keys($this->ribbonBinObjects['data']); + $ReturnData = array_unique(array_map(fn (string $path): string => pathinfo($path, PATHINFO_EXTENSION), $tmpTypes)); + } else { + $ReturnData = []; // the caller want an array... not null if empty + } + + break; + } + + return $ReturnData; + } + + /** + * This workbook have a custom UI ? + */ + public function hasRibbon(): bool + { + return $this->ribbonXMLData !== null; + } + + /** + * This workbook have additional object for the ribbon ? + */ + public function hasRibbonBinObjects(): bool + { + return $this->ribbonBinObjects !== null; + } + + /** + * Check if a sheet with a specified code name already exists. + * + * @param string $codeName Name of the worksheet to check + */ + public function sheetCodeNameExists(string $codeName): bool + { + return $this->getSheetByCodeName($codeName) !== null; + } + + /** + * Get sheet by code name. Warning : sheet don't have always a code name ! + * + * @param string $codeName Sheet name + */ + public function getSheetByCodeName(string $codeName): ?Worksheet + { + $worksheetCount = count($this->workSheetCollection); + for ($i = 0; $i < $worksheetCount; ++$i) { + if ($this->workSheetCollection[$i]->getCodeName() == $codeName) { + return $this->workSheetCollection[$i]; + } + } + + return null; + } + + /** + * Create a new PhpSpreadsheet with one Worksheet. + */ + public function __construct() + { + $this->uniqueID = uniqid('', true); + $this->calculationEngine = new Calculation($this); + $this->theme = new Theme(); + + // Initialise worksheet collection and add one worksheet + $this->workSheetCollection = []; + $this->workSheetCollection[] = new Worksheet($this); + $this->activeSheetIndex = 0; + + // Create document properties + $this->properties = new Properties(); + + // Create document security + $this->security = new Security(); + + // Set defined names + $this->definedNames = []; + + // Create the cellXf supervisor + $this->cellXfSupervisor = new Style(true); + $this->cellXfSupervisor->bindParent($this); + + // Create the default style + $this->addCellXf(new Style()); + $this->addCellStyleXf(new Style()); + } + + /** + * Code to execute when this worksheet is unset(). + */ + public function __destruct() + { + $this->disconnectWorksheets(); + unset($this->calculationEngine); + $this->cellXfCollection = []; + $this->cellStyleXfCollection = []; + $this->definedNames = []; + } + + /** + * Disconnect all worksheets from this PhpSpreadsheet workbook object, + * typically so that the PhpSpreadsheet object can be unset. + */ + public function disconnectWorksheets(): void + { + foreach ($this->workSheetCollection as $worksheet) { + $worksheet->disconnectCells(); + unset($worksheet); + } + $this->workSheetCollection = []; + } + + /** + * Return the calculation engine for this worksheet. + */ + public function getCalculationEngine(): Calculation + { + return $this->calculationEngine; + } + + /** + * Intended for use only via a destructor. + * + * @internal + */ + public function getCalculationEngineOrNull(): ?Calculation + { + if (!isset($this->calculationEngine)) { //* @phpstan-ignore-line + return null; + } + + return $this->calculationEngine; + } + + /** + * Get properties. + */ + public function getProperties(): Properties + { + return $this->properties; + } + + /** + * Set properties. + */ + public function setProperties(Properties $documentProperties): void + { + $this->properties = $documentProperties; + } + + /** + * Get security. + */ + public function getSecurity(): Security + { + return $this->security; + } + + /** + * Set security. + */ + public function setSecurity(Security $documentSecurity): void + { + $this->security = $documentSecurity; + } + + /** + * Get active sheet. + */ + public function getActiveSheet(): Worksheet + { + return $this->getSheet($this->activeSheetIndex); + } + + /** + * Create sheet and add it to this workbook. + * + * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) + */ + public function createSheet(?int $sheetIndex = null): Worksheet + { + $newSheet = new Worksheet($this); + $this->addSheet($newSheet, $sheetIndex, true); + + return $newSheet; + } + + /** + * Check if a sheet with a specified name already exists. + * + * @param string $worksheetName Name of the worksheet to check + */ + public function sheetNameExists(string $worksheetName): bool + { + return $this->getSheetByName($worksheetName) !== null; + } + + public function duplicateWorksheetByTitle(string $title): Worksheet + { + $original = $this->getSheetByNameOrThrow($title); + $index = $this->getIndex($original) + 1; + $clone = clone $original; + + return $this->addSheet($clone, $index, true); + } + + /** + * Add sheet. + * + * @param Worksheet $worksheet The worksheet to add + * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) + */ + public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null, bool $retitleIfNeeded = false): Worksheet + { + if ($retitleIfNeeded) { + $title = $worksheet->getTitle(); + if ($this->sheetNameExists($title)) { + $i = 1; + $newTitle = "$title $i"; + while ($this->sheetNameExists($newTitle)) { + ++$i; + $newTitle = "$title $i"; + } + $worksheet->setTitle($newTitle); + } + } + if ($this->sheetNameExists($worksheet->getTitle())) { + throw new Exception( + "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first." + ); + } + + if ($sheetIndex === null) { + if ($this->activeSheetIndex < 0) { + $this->activeSheetIndex = 0; + } + $this->workSheetCollection[] = $worksheet; + } else { + // Insert the sheet at the requested index + array_splice( + $this->workSheetCollection, + $sheetIndex, + 0, + [$worksheet] + ); + + // Adjust active sheet index if necessary + if ($this->activeSheetIndex >= $sheetIndex) { + ++$this->activeSheetIndex; + } + if ($this->activeSheetIndex < 0) { + $this->activeSheetIndex = 0; + } + } + + if ($worksheet->getParent() === null) { + $worksheet->rebindParent($this); + } + + return $worksheet; + } + + /** + * Remove sheet by index. + * + * @param int $sheetIndex Index position of the worksheet to remove + */ + public function removeSheetByIndex(int $sheetIndex): void + { + $numSheets = count($this->workSheetCollection); + if ($sheetIndex > $numSheets - 1) { + throw new Exception( + "You tried to remove a sheet by the out of bounds index: {$sheetIndex}. The actual number of sheets is {$numSheets}." + ); + } + array_splice($this->workSheetCollection, $sheetIndex, 1); + + // Adjust active sheet index if necessary + if ( + ($this->activeSheetIndex >= $sheetIndex) + && ($this->activeSheetIndex > 0 || $numSheets <= 1) + ) { + --$this->activeSheetIndex; + } + } + + /** + * Get sheet by index. + * + * @param int $sheetIndex Sheet index + */ + public function getSheet(int $sheetIndex): Worksheet + { + if (!isset($this->workSheetCollection[$sheetIndex])) { + $numSheets = $this->getSheetCount(); + + throw new Exception( + "Your requested sheet index: {$sheetIndex} is out of bounds. The actual number of sheets is {$numSheets}." + ); + } + + return $this->workSheetCollection[$sheetIndex]; + } + + /** + * Get all sheets. + * + * @return Worksheet[] + */ + public function getAllSheets(): array + { + return $this->workSheetCollection; + } + + /** + * Get sheet by name. + * + * @param string $worksheetName Sheet name + */ + public function getSheetByName(string $worksheetName): ?Worksheet + { + $trimWorksheetName = StringHelper::strToUpper(trim($worksheetName, "'")); + foreach ($this->workSheetCollection as $worksheet) { + if (StringHelper::strToUpper($worksheet->getTitle()) === $trimWorksheetName) { + return $worksheet; + } + } + + return null; + } + + /** + * Get sheet by name, throwing exception if not found. + */ + public function getSheetByNameOrThrow(string $worksheetName): Worksheet + { + $worksheet = $this->getSheetByName($worksheetName); + if ($worksheet === null) { + throw new Exception("Sheet $worksheetName does not exist."); + } + + return $worksheet; + } + + /** + * Get index for sheet. + * + * @return int index + */ + public function getIndex(Worksheet $worksheet, bool $noThrow = false): int + { + foreach ($this->workSheetCollection as $key => $value) { + if ($value === $worksheet) { + return $key; + } + } + if ($noThrow) { + return -1; + } + + throw new Exception('Sheet does not exist.'); + } + + /** + * Set index for sheet by sheet name. + * + * @param string $worksheetName Sheet name to modify index for + * @param int $newIndexPosition New index for the sheet + * + * @return int New sheet index + */ + public function setIndexByName(string $worksheetName, int $newIndexPosition): int + { + $oldIndex = $this->getIndex($this->getSheetByNameOrThrow($worksheetName)); + $worksheet = array_splice( + $this->workSheetCollection, + $oldIndex, + 1 + ); + array_splice( + $this->workSheetCollection, + $newIndexPosition, + 0, + $worksheet + ); + + return $newIndexPosition; + } + + /** + * Get sheet count. + */ + public function getSheetCount(): int + { + return count($this->workSheetCollection); + } + + /** + * Get active sheet index. + * + * @return int Active sheet index + */ + public function getActiveSheetIndex(): int + { + return $this->activeSheetIndex; + } + + /** + * Set active sheet index. + * + * @param int $worksheetIndex Active sheet index + */ + public function setActiveSheetIndex(int $worksheetIndex): Worksheet + { + $numSheets = count($this->workSheetCollection); + + if ($worksheetIndex > $numSheets - 1) { + throw new Exception( + "You tried to set a sheet active by the out of bounds index: {$worksheetIndex}. The actual number of sheets is {$numSheets}." + ); + } + $this->activeSheetIndex = $worksheetIndex; + + return $this->getActiveSheet(); + } + + /** + * Set active sheet index by name. + * + * @param string $worksheetName Sheet title + */ + public function setActiveSheetIndexByName(string $worksheetName): Worksheet + { + if (($worksheet = $this->getSheetByName($worksheetName)) instanceof Worksheet) { + $this->setActiveSheetIndex($this->getIndex($worksheet)); + + return $worksheet; + } + + throw new Exception('Workbook does not contain sheet:' . $worksheetName); + } + + /** + * Get sheet names. + * + * @return string[] + */ + public function getSheetNames(): array + { + $returnValue = []; + $worksheetCount = $this->getSheetCount(); + for ($i = 0; $i < $worksheetCount; ++$i) { + $returnValue[] = $this->getSheet($i)->getTitle(); + } + + return $returnValue; + } + + /** + * Add external sheet. + * + * @param Worksheet $worksheet External sheet to add + * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) + */ + public function addExternalSheet(Worksheet $worksheet, ?int $sheetIndex = null): Worksheet + { + if ($this->sheetNameExists($worksheet->getTitle())) { + throw new Exception("Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename the external sheet first."); + } + + // count how many cellXfs there are in this workbook currently, we will need this below + $countCellXfs = count($this->cellXfCollection); + + // copy all the shared cellXfs from the external workbook and append them to the current + foreach ($worksheet->getParentOrThrow()->getCellXfCollection() as $cellXf) { + $this->addCellXf(clone $cellXf); + } + + // move sheet to this workbook + $worksheet->rebindParent($this); + + // update the cellXfs + foreach ($worksheet->getCoordinates(false) as $coordinate) { + $cell = $worksheet->getCell($coordinate); + $cell->setXfIndex($cell->getXfIndex() + $countCellXfs); + } + + // update the column dimensions Xfs + foreach ($worksheet->getColumnDimensions() as $columnDimension) { + $columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs); + } + + // update the row dimensions Xfs + foreach ($worksheet->getRowDimensions() as $rowDimension) { + $xfIndex = $rowDimension->getXfIndex(); + if ($xfIndex !== null) { + $rowDimension->setXfIndex($xfIndex + $countCellXfs); + } + } + + return $this->addSheet($worksheet, $sheetIndex); + } + + /** + * Get an array of all Named Ranges. + * + * @return DefinedName[] + */ + public function getNamedRanges(): array + { + return array_filter( + $this->definedNames, + fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_RANGE + ); + } + + /** + * Get an array of all Named Formulae. + * + * @return DefinedName[] + */ + public function getNamedFormulae(): array + { + return array_filter( + $this->definedNames, + fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_FORMULA + ); + } + + /** + * Get an array of all Defined Names (both named ranges and named formulae). + * + * @return DefinedName[] + */ + public function getDefinedNames(): array + { + return $this->definedNames; + } + + /** + * Add a named range. + * If a named range with this name already exists, then this will replace the existing value. + */ + public function addNamedRange(NamedRange $namedRange): void + { + $this->addDefinedName($namedRange); + } + + /** + * Add a named formula. + * If a named formula with this name already exists, then this will replace the existing value. + */ + public function addNamedFormula(NamedFormula $namedFormula): void + { + $this->addDefinedName($namedFormula); + } + + /** + * Add a defined name (either a named range or a named formula). + * If a defined named with this name already exists, then this will replace the existing value. + */ + public function addDefinedName(DefinedName $definedName): void + { + $upperCaseName = StringHelper::strToUpper($definedName->getName()); + if ($definedName->getScope() == null) { + // global scope + $this->definedNames[$upperCaseName] = $definedName; + } else { + // local scope + $this->definedNames[$definedName->getScope()->getTitle() . '!' . $upperCaseName] = $definedName; + } + } + + /** + * Get named range. + * + * @param null|Worksheet $worksheet Scope. Use null for global scope + */ + public function getNamedRange(string $namedRange, ?Worksheet $worksheet = null): ?NamedRange + { + $returnValue = null; + + if ($namedRange !== '') { + $namedRange = StringHelper::strToUpper($namedRange); + // first look for global named range + $returnValue = $this->getGlobalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE); + // then look for local named range (has priority over global named range if both names exist) + $returnValue = $this->getLocalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE, $worksheet) ?: $returnValue; + } + + return $returnValue instanceof NamedRange ? $returnValue : null; + } + + /** + * Get named formula. + * + * @param null|Worksheet $worksheet Scope. Use null for global scope + */ + public function getNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): ?NamedFormula + { + $returnValue = null; + + if ($namedFormula !== '') { + $namedFormula = StringHelper::strToUpper($namedFormula); + // first look for global named formula + $returnValue = $this->getGlobalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA); + // then look for local named formula (has priority over global named formula if both names exist) + $returnValue = $this->getLocalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA, $worksheet) ?: $returnValue; + } + + return $returnValue instanceof NamedFormula ? $returnValue : null; + } + + private function getGlobalDefinedNameByType(string $name, bool $type): ?DefinedName + { + if (isset($this->definedNames[$name]) && $this->definedNames[$name]->isFormula() === $type) { + return $this->definedNames[$name]; + } + + return null; + } + + private function getLocalDefinedNameByType(string $name, bool $type, ?Worksheet $worksheet = null): ?DefinedName + { + if ( + ($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $name]) + && $this->definedNames[$worksheet->getTitle() . '!' . $name]->isFormula() === $type + ) { + return $this->definedNames[$worksheet->getTitle() . '!' . $name]; + } + + return null; + } + + /** + * Get named range. + * + * @param null|Worksheet $worksheet Scope. Use null for global scope + */ + public function getDefinedName(string $definedName, ?Worksheet $worksheet = null): ?DefinedName + { + $returnValue = null; + + if ($definedName !== '') { + $definedName = StringHelper::strToUpper($definedName); + // first look for global defined name + foreach ($this->definedNames as $dn) { + $upper = StringHelper::strToUpper($dn->getName()); + if ( + !$dn->getLocalOnly() + && $definedName === $upper + ) { + $returnValue = $dn; + + break; + } + } + + // then look for local defined name (has priority over global defined name if both names exist) + if ($worksheet !== null) { + $wsTitle = StringHelper::strToUpper($worksheet->getTitle()); + $definedName = (string) preg_replace('/^.*!/', '', $definedName); + foreach ($this->definedNames as $dn) { + $sheet = $dn->getScope() ?? $dn->getWorksheet(); + $upper = StringHelper::strToUpper($dn->getName()); + $upperTitle = StringHelper::strToUpper((string) $sheet?->getTitle()); + if ( + $dn->getLocalOnly() + && $upper === $definedName + && $upperTitle === $wsTitle + ) { + return $dn; + } + } + } + } + + return $returnValue; + } + + /** + * Remove named range. + * + * @param null|Worksheet $worksheet scope: use null for global scope + * + * @return $this + */ + public function removeNamedRange(string $namedRange, ?Worksheet $worksheet = null): self + { + if ($this->getNamedRange($namedRange, $worksheet) === null) { + return $this; + } + + return $this->removeDefinedName($namedRange, $worksheet); + } + + /** + * Remove named formula. + * + * @param null|Worksheet $worksheet scope: use null for global scope + * + * @return $this + */ + public function removeNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): self + { + if ($this->getNamedFormula($namedFormula, $worksheet) === null) { + return $this; + } + + return $this->removeDefinedName($namedFormula, $worksheet); + } + + /** + * Remove defined name. + * + * @param null|Worksheet $worksheet scope: use null for global scope + * + * @return $this + */ + public function removeDefinedName(string $definedName, ?Worksheet $worksheet = null): self + { + $definedName = StringHelper::strToUpper($definedName); + + if ($worksheet === null) { + if (isset($this->definedNames[$definedName])) { + unset($this->definedNames[$definedName]); + } + } else { + if (isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) { + unset($this->definedNames[$worksheet->getTitle() . '!' . $definedName]); + } elseif (isset($this->definedNames[$definedName])) { + unset($this->definedNames[$definedName]); + } + } + + return $this; + } + + /** + * Get worksheet iterator. + */ + public function getWorksheetIterator(): Iterator + { + return new Iterator($this); + } + + /** + * Copy workbook (!= clone!). + */ + public function copy(): self + { + return unserialize(serialize($this)); //* @phpstan-ignore-line + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->uniqueID = uniqid('', true); + + $usedKeys = []; + // I don't know why new Style rather than clone. + $this->cellXfSupervisor = new Style(true); + //$this->cellXfSupervisor = clone $this->cellXfSupervisor; + $this->cellXfSupervisor->bindParent($this); + $usedKeys['cellXfSupervisor'] = true; + + $oldCalc = $this->calculationEngine; + $this->calculationEngine = new Calculation($this); + $this->calculationEngine + ->setSuppressFormulaErrors( + $oldCalc->getSuppressFormulaErrors() + ) + ->setCalculationCacheEnabled( + $oldCalc->getCalculationCacheEnabled() + ) + ->setBranchPruningEnabled( + $oldCalc->getBranchPruningEnabled() + ) + ->setInstanceArrayReturnType( + $oldCalc->getInstanceArrayReturnType() + ); + $usedKeys['calculationEngine'] = true; + + $currentCollection = $this->cellStyleXfCollection; + $this->cellStyleXfCollection = []; + foreach ($currentCollection as $item) { + $clone = $item->exportArray(); + $style = (new Style())->applyFromArray($clone); + $this->addCellStyleXf($style); + } + $usedKeys['cellStyleXfCollection'] = true; + + $currentCollection = $this->cellXfCollection; + $this->cellXfCollection = []; + foreach ($currentCollection as $item) { + $clone = $item->exportArray(); + $style = (new Style())->applyFromArray($clone); + $this->addCellXf($style); + } + $usedKeys['cellXfCollection'] = true; + + $currentCollection = $this->workSheetCollection; + $this->workSheetCollection = []; + foreach ($currentCollection as $item) { + $clone = clone $item; + $clone->setParent($this); + $this->workSheetCollection[] = $clone; + } + $usedKeys['workSheetCollection'] = true; + + foreach (get_object_vars($this) as $key => $val) { + if (isset($usedKeys[$key])) { + continue; + } + switch ($key) { + // arrays of objects not covered above + case 'definedNames': + /** @var DefinedName[] */ + $currentCollection = $val; + $this->$key = []; + foreach ($currentCollection as $item) { + $clone = clone $item; + $this->{$key}[] = $clone; + } + + break; + default: + if (is_object($val)) { + $this->$key = clone $val; + } + } + } + } + + /** + * Get the workbook collection of cellXfs. + * + * @return Style[] + */ + public function getCellXfCollection(): array + { + return $this->cellXfCollection; + } + + /** + * Get cellXf by index. + */ + public function getCellXfByIndex(int $cellStyleIndex): Style + { + return $this->cellXfCollection[$cellStyleIndex]; + } + + public function getCellXfByIndexOrNull(?int $cellStyleIndex): ?Style + { + return ($cellStyleIndex === null) ? null : ($this->cellXfCollection[$cellStyleIndex] ?? null); + } + + /** + * Get cellXf by hash code. + * + * @return false|Style + */ + public function getCellXfByHashCode(string $hashcode): bool|Style + { + foreach ($this->cellXfCollection as $cellXf) { + if ($cellXf->getHashCode() === $hashcode) { + return $cellXf; + } + } + + return false; + } + + /** + * Check if style exists in style collection. + */ + public function cellXfExists(Style $cellStyleIndex): bool + { + return in_array($cellStyleIndex, $this->cellXfCollection, true); + } + + /** + * Get default style. + */ + public function getDefaultStyle(): Style + { + if (isset($this->cellXfCollection[0])) { + return $this->cellXfCollection[0]; + } + + throw new Exception('No default style found for this workbook'); + } + + /** + * Add a cellXf to the workbook. + */ + public function addCellXf(Style $style): void + { + $this->cellXfCollection[] = $style; + $style->setIndex(count($this->cellXfCollection) - 1); + } + + /** + * Remove cellXf by index. It is ensured that all cells get their xf index updated. + * + * @param int $cellStyleIndex Index to cellXf + */ + public function removeCellXfByIndex(int $cellStyleIndex): void + { + if ($cellStyleIndex > count($this->cellXfCollection) - 1) { + throw new Exception('CellXf index is out of bounds.'); + } + + // first remove the cellXf + array_splice($this->cellXfCollection, $cellStyleIndex, 1); + + // then update cellXf indexes for cells + foreach ($this->workSheetCollection as $worksheet) { + foreach ($worksheet->getCoordinates(false) as $coordinate) { + $cell = $worksheet->getCell($coordinate); + $xfIndex = $cell->getXfIndex(); + if ($xfIndex > $cellStyleIndex) { + // decrease xf index by 1 + $cell->setXfIndex($xfIndex - 1); + } elseif ($xfIndex == $cellStyleIndex) { + // set to default xf index 0 + $cell->setXfIndex(0); + } + } + } + } + + /** + * Get the cellXf supervisor. + */ + public function getCellXfSupervisor(): Style + { + return $this->cellXfSupervisor; + } + + /** + * Get the workbook collection of cellStyleXfs. + * + * @return Style[] + */ + public function getCellStyleXfCollection(): array + { + return $this->cellStyleXfCollection; + } + + /** + * Get cellStyleXf by index. + * + * @param int $cellStyleIndex Index to cellXf + */ + public function getCellStyleXfByIndex(int $cellStyleIndex): Style + { + return $this->cellStyleXfCollection[$cellStyleIndex]; + } + + /** + * Get cellStyleXf by hash code. + * + * @return false|Style + */ + public function getCellStyleXfByHashCode(string $hashcode): bool|Style + { + foreach ($this->cellStyleXfCollection as $cellStyleXf) { + if ($cellStyleXf->getHashCode() === $hashcode) { + return $cellStyleXf; + } + } + + return false; + } + + /** + * Add a cellStyleXf to the workbook. + */ + public function addCellStyleXf(Style $style): void + { + $this->cellStyleXfCollection[] = $style; + $style->setIndex(count($this->cellStyleXfCollection) - 1); + } + + /** + * Remove cellStyleXf by index. + * + * @param int $cellStyleIndex Index to cellXf + */ + public function removeCellStyleXfByIndex(int $cellStyleIndex): void + { + if ($cellStyleIndex > count($this->cellStyleXfCollection) - 1) { + throw new Exception('CellStyleXf index is out of bounds.'); + } + array_splice($this->cellStyleXfCollection, $cellStyleIndex, 1); + } + + /** + * Eliminate all unneeded cellXf and afterwards update the xfIndex for all cells + * and columns in the workbook. + */ + public function garbageCollect(): void + { + // how many references are there to each cellXf ? + $countReferencesCellXf = []; + foreach ($this->cellXfCollection as $index => $cellXf) { + $countReferencesCellXf[$index] = 0; + } + + foreach ($this->getWorksheetIterator() as $sheet) { + // from cells + foreach ($sheet->getCoordinates(false) as $coordinate) { + $cell = $sheet->getCell($coordinate); + ++$countReferencesCellXf[$cell->getXfIndex()]; + } + + // from row dimensions + foreach ($sheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getXfIndex() !== null) { + ++$countReferencesCellXf[$rowDimension->getXfIndex()]; + } + } + + // from column dimensions + foreach ($sheet->getColumnDimensions() as $columnDimension) { + ++$countReferencesCellXf[$columnDimension->getXfIndex()]; + } + } + + // remove cellXfs without references and create mapping so we can update xfIndex + // for all cells and columns + $countNeededCellXfs = 0; + $map = []; + foreach ($this->cellXfCollection as $index => $cellXf) { + if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf + ++$countNeededCellXfs; + } else { + unset($this->cellXfCollection[$index]); + } + $map[$index] = $countNeededCellXfs - 1; + } + $this->cellXfCollection = array_values($this->cellXfCollection); + + // update the index for all cellXfs + foreach ($this->cellXfCollection as $i => $cellXf) { + $cellXf->setIndex($i); + } + + // make sure there is always at least one cellXf (there should be) + if (empty($this->cellXfCollection)) { + $this->cellXfCollection[] = new Style(); + } + + // update the xfIndex for all cells, row dimensions, column dimensions + foreach ($this->getWorksheetIterator() as $sheet) { + // for all cells + foreach ($sheet->getCoordinates(false) as $coordinate) { + $cell = $sheet->getCell($coordinate); + $cell->setXfIndex($map[$cell->getXfIndex()]); + } + + // for all row dimensions + foreach ($sheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getXfIndex() !== null) { + $rowDimension->setXfIndex($map[$rowDimension->getXfIndex()]); + } + } + + // for all column dimensions + foreach ($sheet->getColumnDimensions() as $columnDimension) { + $columnDimension->setXfIndex($map[$columnDimension->getXfIndex()]); + } + + // also do garbage collection for all the sheets + $sheet->garbageCollect(); + } + } + + /** + * Return the unique ID value assigned to this spreadsheet workbook. + * + * @deprecated 5.2.0 Serves no useful purpose. No replacement. + * + * @codeCoverageIgnore + */ + public function getID(): string + { + return $this->uniqueID; + } + + /** + * Get the visibility of the horizonal scroll bar in the application. + * + * @return bool True if horizonal scroll bar is visible + */ + public function getShowHorizontalScroll(): bool + { + return $this->showHorizontalScroll; + } + + /** + * Set the visibility of the horizonal scroll bar in the application. + * + * @param bool $showHorizontalScroll True if horizonal scroll bar is visible + */ + public function setShowHorizontalScroll(bool $showHorizontalScroll): void + { + $this->showHorizontalScroll = (bool) $showHorizontalScroll; + } + + /** + * Get the visibility of the vertical scroll bar in the application. + * + * @return bool True if vertical scroll bar is visible + */ + public function getShowVerticalScroll(): bool + { + return $this->showVerticalScroll; + } + + /** + * Set the visibility of the vertical scroll bar in the application. + * + * @param bool $showVerticalScroll True if vertical scroll bar is visible + */ + public function setShowVerticalScroll(bool $showVerticalScroll): void + { + $this->showVerticalScroll = (bool) $showVerticalScroll; + } + + /** + * Get the visibility of the sheet tabs in the application. + * + * @return bool True if the sheet tabs are visible + */ + public function getShowSheetTabs(): bool + { + return $this->showSheetTabs; + } + + /** + * Set the visibility of the sheet tabs in the application. + * + * @param bool $showSheetTabs True if sheet tabs are visible + */ + public function setShowSheetTabs(bool $showSheetTabs): void + { + $this->showSheetTabs = (bool) $showSheetTabs; + } + + /** + * Return whether the workbook window is minimized. + * + * @return bool true if workbook window is minimized + */ + public function getMinimized(): bool + { + return $this->minimized; + } + + /** + * Set whether the workbook window is minimized. + * + * @param bool $minimized true if workbook window is minimized + */ + public function setMinimized(bool $minimized): void + { + $this->minimized = (bool) $minimized; + } + + /** + * Return whether to group dates when presenting the user with + * filtering options in the user interface. + * + * @return bool true if workbook window is minimized + */ + public function getAutoFilterDateGrouping(): bool + { + return $this->autoFilterDateGrouping; + } + + /** + * Set whether to group dates when presenting the user with + * filtering options in the user interface. + * + * @param bool $autoFilterDateGrouping true if workbook window is minimized + */ + public function setAutoFilterDateGrouping(bool $autoFilterDateGrouping): void + { + $this->autoFilterDateGrouping = (bool) $autoFilterDateGrouping; + } + + /** + * Return the first sheet in the book view. + * + * @return int First sheet in book view + */ + public function getFirstSheetIndex(): int + { + return $this->firstSheetIndex; + } + + /** + * Set the first sheet in the book view. + * + * @param int $firstSheetIndex First sheet in book view + */ + public function setFirstSheetIndex(int $firstSheetIndex): void + { + if ($firstSheetIndex >= 0) { + $this->firstSheetIndex = (int) $firstSheetIndex; + } else { + throw new Exception('First sheet index must be a positive integer.'); + } + } + + /** + * Return the visibility status of the workbook. + * + * This may be one of the following three values: + * - visibile + * + * @return string Visible status + */ + public function getVisibility(): string + { + return $this->visibility; + } + + /** + * Set the visibility status of the workbook. + * + * Valid values are: + * - 'visible' (self::VISIBILITY_VISIBLE): + * Workbook window is visible + * - 'hidden' (self::VISIBILITY_HIDDEN): + * Workbook window is hidden, but can be shown by the user + * via the user interface + * - 'veryHidden' (self::VISIBILITY_VERY_HIDDEN): + * Workbook window is hidden and cannot be shown in the + * user interface. + * + * @param null|string $visibility visibility status of the workbook + */ + public function setVisibility(?string $visibility): void + { + if ($visibility === null) { + $visibility = self::VISIBILITY_VISIBLE; + } + + if (in_array($visibility, self::WORKBOOK_VIEW_VISIBILITY_VALUES)) { + $this->visibility = $visibility; + } else { + throw new Exception('Invalid visibility value.'); + } + } + + /** + * Get the ratio between the workbook tabs bar and the horizontal scroll bar. + * TabRatio is assumed to be out of 1000 of the horizontal window width. + * + * @return int Ratio between the workbook tabs bar and the horizontal scroll bar + */ + public function getTabRatio(): int + { + return $this->tabRatio; + } + + /** + * Set the ratio between the workbook tabs bar and the horizontal scroll bar + * TabRatio is assumed to be out of 1000 of the horizontal window width. + * + * @param int $tabRatio Ratio between the tabs bar and the horizontal scroll bar + */ + public function setTabRatio(int $tabRatio): void + { + if ($tabRatio >= 0 && $tabRatio <= 1000) { + $this->tabRatio = (int) $tabRatio; + } else { + throw new Exception('Tab ratio must be between 0 and 1000.'); + } + } + + public function reevaluateAutoFilters(bool $resetToMax): void + { + foreach ($this->workSheetCollection as $sheet) { + $filter = $sheet->getAutoFilter(); + if (!empty($filter->getRange())) { + if ($resetToMax) { + $filter->setRangeToMaxRow(); + } + $filter->showHideRows(); + } + } + } + + /** + * @throws Exception + */ + public function jsonSerialize(): mixed + { + throw new Exception('Spreadsheet objects cannot be json encoded'); + } + + public function resetThemeFonts(): void + { + $majorFontLatin = $this->theme->getMajorFontLatin(); + $minorFontLatin = $this->theme->getMinorFontLatin(); + foreach ($this->cellXfCollection as $cellStyleXf) { + $scheme = $cellStyleXf->getFont()->getScheme(); + if ($scheme === 'major') { + $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme); + } elseif ($scheme === 'minor') { + $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme); + } + } + foreach ($this->cellStyleXfCollection as $cellStyleXf) { + $scheme = $cellStyleXf->getFont()->getScheme(); + if ($scheme === 'major') { + $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme); + } elseif ($scheme === 'minor') { + $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme); + } + } + } + + public function getTableByName(string $tableName): ?Table + { + $table = null; + foreach ($this->workSheetCollection as $sheet) { + $table = $sheet->getTableByName($tableName); + if ($table !== null) { + break; + } + } + + return $table; + } + + /** + * @return bool Success or failure + */ + public function setExcelCalendar(int $baseYear): bool + { + if (($baseYear === Date::CALENDAR_WINDOWS_1900) || ($baseYear === Date::CALENDAR_MAC_1904)) { + $this->excelCalendar = $baseYear; + + return true; + } + + return false; + } + + /** + * @return int Excel base date (1900 or 1904) + */ + public function getExcelCalendar(): int + { + return $this->excelCalendar; + } + + public function deleteLegacyDrawing(Worksheet $worksheet): void + { + unset($this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing']); + } + + public function getLegacyDrawing(Worksheet $worksheet): ?string + { + /** @var ?string */ + $temp = $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null; + + return $temp; + } + + public function getValueBinder(): ?IValueBinder + { + return $this->valueBinder; + } + + public function setValueBinder(?IValueBinder $valueBinder): self + { + $this->valueBinder = $valueBinder; + + return $this; + } + + /** + * All the PDF writers treat charts as if they occupy a single cell. + * This will be better most of the time. + * It is not needed for any other output type. + * It changes the contents of the spreadsheet, so you might + * be better off cloning the spreadsheet and then using + * this method on, and then writing, the clone. + */ + public function mergeChartCellsForPdf(): void + { + foreach ($this->workSheetCollection as $worksheet) { + foreach ($worksheet->getChartCollection() as $chart) { + $br = $chart->getBottomRightCell(); + $tl = $chart->getTopLeftCell(); + if ($br !== '' && $br !== $tl) { + if (!$worksheet->cellExists($br)) { + $worksheet->getCell($br)->setValue(' '); + } + $worksheet->mergeCells("$tl:$br"); + } + } + } + } + + /** + * All the PDF writers do better with drawings than charts. + * This will be better some of the time. + * It is not needed for any other output type. + * It changes the contents of the spreadsheet, so you might + * be better off cloning the spreadsheet and then using + * this method on, and then writing, the clone. + */ + public function mergeDrawingCellsForPdf(): void + { + foreach ($this->workSheetCollection as $worksheet) { + foreach ($worksheet->getDrawingCollection() as $drawing) { + $br = $drawing->getCoordinates2(); + $tl = $drawing->getCoordinates(); + if ($br !== '' && $br !== $tl) { + if (!$worksheet->cellExists($br)) { + $worksheet->getCell($br)->setValue(' '); + } + $worksheet->mergeCells("$tl:$br"); + } + } + } + } + + /** + * Excel will sometimes replace user's formatting choice + * with a built-in choice that it thinks is equivalent. + * Its choice is often not equivalent after all. + * Such treatment is astonishingly user-hostile. + * This function will undo such changes. + */ + public function replaceBuiltinNumberFormat(int $builtinFormatIndex, string $formatCode): void + { + foreach ($this->cellXfCollection as $style) { + $numberFormat = $style->getNumberFormat(); + if ($numberFormat->getBuiltInFormatCode() === $builtinFormatIndex) { + $numberFormat->setFormatCode($formatCode); + } + } + } + + public function returnArrayAsArray(): void + { + $this->calculationEngine->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + } + + public function returnArrayAsValue(): void + { + $this->calculationEngine->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_VALUE + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Alignment.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Alignment.php new file mode 100644 index 0000000..7d3eb44 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Alignment.php @@ -0,0 +1,548 @@ + self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER_CONTINUOUS, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_DISTRIBUTED, + ]; + // Mapping for horizontal alignment CSS + const HORIZONTAL_ALIGNMENT_FOR_HTML = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + //self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, // no reasonable equivalent for fill + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_JUSTIFY, + ]; + + // Vertical alignment styles + const VERTICAL_BOTTOM = 'bottom'; + const VERTICAL_TOP = 'top'; + const VERTICAL_CENTER = 'center'; + const VERTICAL_JUSTIFY = 'justify'; + const VERTICAL_DISTRIBUTED = 'distributed'; // Excel2007 only + // Vertical alignment CSS + private const VERTICAL_BASELINE = 'baseline'; + private const VERTICAL_MIDDLE = 'middle'; + private const VERTICAL_SUB = 'sub'; + private const VERTICAL_SUPER = 'super'; + private const VERTICAL_TEXT_BOTTOM = 'text-bottom'; + private const VERTICAL_TEXT_TOP = 'text-top'; + + // Mapping for vertical alignment + const VERTICAL_ALIGNMENT_FOR_XLSX = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_CENTER, + self::VERTICAL_JUSTIFY => self::VERTICAL_JUSTIFY, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_DISTRIBUTED, + // css settings that aren't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BOTTOM, + self::VERTICAL_MIDDLE => self::VERTICAL_CENTER, + self::VERTICAL_SUB => self::VERTICAL_BOTTOM, + self::VERTICAL_SUPER => self::VERTICAL_TOP, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TOP, + ]; + + // Mapping for vertical alignment for Html + const VERTICAL_ALIGNMENT_FOR_HTML = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_MIDDLE, + self::VERTICAL_JUSTIFY => self::VERTICAL_MIDDLE, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_MIDDLE, + // css settings that aren't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BASELINE, + self::VERTICAL_MIDDLE => self::VERTICAL_MIDDLE, + self::VERTICAL_SUB => self::VERTICAL_SUB, + self::VERTICAL_SUPER => self::VERTICAL_SUPER, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_TEXT_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TEXT_TOP, + ]; + + // Read order + const READORDER_CONTEXT = 0; + const READORDER_LTR = 1; + const READORDER_RTL = 2; + + // Special value for Text Rotation + const TEXTROTATION_STACK_EXCEL = 255; + const TEXTROTATION_STACK_PHPSPREADSHEET = -165; // 90 - 255 + + public const INDENT_UNITS_TO_PIXELS = 9; + + /** + * Horizontal alignment. + */ + protected ?string $horizontal = self::HORIZONTAL_GENERAL; + + /** + * Justify Last Line alignment. + */ + protected ?bool $justifyLastLine = null; + + /** + * Vertical alignment. + */ + protected ?string $vertical = self::VERTICAL_BOTTOM; + + /** + * Text rotation. + */ + protected ?int $textRotation = 0; + + /** + * Wrap text. + */ + protected bool $wrapText = false; + + /** + * Shrink to fit. + */ + protected bool $shrinkToFit = false; + + /** + * Indent - only possible with horizontal alignment left and right. + */ + protected int $indent = 0; + + /** + * Read order. + */ + protected int $readOrder = 0; + + /** + * Create a new Alignment. + * + * @param bool $isSupervisor Flag indicating if this is a supervisor or not + * Leave this value at default unless you understand exactly what + * its ramifications are + * @param bool $isConditional Flag indicating if this is a conditional style or not + * Leave this value at default unless you understand exactly what + * its ramifications are + */ + public function __construct(bool $isSupervisor = false, bool $isConditional = false) + { + // Supervisor? + parent::__construct($isSupervisor); + + if ($isConditional) { + $this->horizontal = null; + $this->vertical = null; + $this->textRotation = null; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getAlignment(); + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return array{alignment: mixed[]} + */ + public function getStyleArray(array $array): array + { + return ['alignment' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getAlignment()->applyFromArray( + * [ + * 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER, + * 'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER, + * 'textRotation' => 0, + * 'wrapText' => TRUE + * ] + * ); + * + * + * @param mixed[] $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells()) + ->applyFromArray($this->getStyleArray($styleArray)); + } else { + /** @var array{horizontal?: string, vertical?: string, justifyLastLine?: bool, textRotation?: int, wrapText?: bool, shrinkToFit?: bool, readOrder?: int, indent?: int} $styleArray */ + if (isset($styleArray['horizontal'])) { + $this->setHorizontal($styleArray['horizontal']); + } + if (isset($styleArray['justifyLastLine'])) { + $this->setJustifyLastLine($styleArray['justifyLastLine']); + } + if (isset($styleArray['vertical'])) { + $this->setVertical($styleArray['vertical']); + } + if (isset($styleArray['textRotation'])) { + $this->setTextRotation($styleArray['textRotation']); + } + if (isset($styleArray['wrapText'])) { + $this->setWrapText($styleArray['wrapText']); + } + if (isset($styleArray['shrinkToFit'])) { + $this->setShrinkToFit($styleArray['shrinkToFit']); + } + if (isset($styleArray['indent'])) { + $this->setIndent($styleArray['indent']); + } + if (isset($styleArray['readOrder'])) { + $this->setReadOrder($styleArray['readOrder']); + } + } + + return $this; + } + + /** + * Get Horizontal. + */ + public function getHorizontal(): null|string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHorizontal(); + } + + return $this->horizontal; + } + + /** + * Set Horizontal. + * + * @param string $horizontalAlignment see self::HORIZONTAL_* + * + * @return $this + */ + public function setHorizontal(string $horizontalAlignment): static + { + $horizontalAlignment = strtolower($horizontalAlignment); + if ($horizontalAlignment === self::HORIZONTAL_CENTER_CONTINUOUS_LC) { + $horizontalAlignment = self::HORIZONTAL_CENTER_CONTINUOUS; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['horizontal' => $horizontalAlignment]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->horizontal = $horizontalAlignment; + } + + return $this; + } + + /** + * Get Justify Last Line. + */ + public function getJustifyLastLine(): ?bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getJustifyLastLine(); + } + + return $this->justifyLastLine; + } + + /** + * Set Justify Last Line. + * + * @return $this + */ + public function setJustifyLastLine(bool $justifyLastLine): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['justifyLastLine' => $justifyLastLine]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->justifyLastLine = $justifyLastLine; + } + + return $this; + } + + /** + * Get Vertical. + */ + public function getVertical(): null|string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getVertical(); + } + + return $this->vertical; + } + + /** + * Set Vertical. + * + * @param string $verticalAlignment see self::VERTICAL_* + * + * @return $this + */ + public function setVertical(string $verticalAlignment): static + { + $verticalAlignment = strtolower($verticalAlignment); + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['vertical' => $verticalAlignment]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->vertical = $verticalAlignment; + } + + return $this; + } + + /** + * Get TextRotation. + */ + public function getTextRotation(): null|int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getTextRotation(); + } + + return $this->textRotation; + } + + /** + * Set TextRotation. + * + * @return $this + */ + public function setTextRotation(int $angleInDegrees): static + { + // Excel2007 value 255 => PhpSpreadsheet value -165 + if ($angleInDegrees == self::TEXTROTATION_STACK_EXCEL) { + $angleInDegrees = self::TEXTROTATION_STACK_PHPSPREADSHEET; + } + + // Set rotation + if (($angleInDegrees >= -90 && $angleInDegrees <= 90) || $angleInDegrees == self::TEXTROTATION_STACK_PHPSPREADSHEET) { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['textRotation' => $angleInDegrees]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->textRotation = $angleInDegrees; + } + } else { + throw new PhpSpreadsheetException('Text rotation should be a value between -90 and 90.'); + } + + return $this; + } + + /** + * Get Wrap Text. + */ + public function getWrapText(): bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getWrapText(); + } + + return $this->wrapText; + } + + /** + * Set Wrap Text. + * + * @return $this + */ + public function setWrapText(bool $wrapped): static + { + if ($wrapped == '') { + $wrapped = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['wrapText' => $wrapped]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->wrapText = $wrapped; + } + + return $this; + } + + /** + * Get Shrink to fit. + */ + public function getShrinkToFit(): bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getShrinkToFit(); + } + + return $this->shrinkToFit; + } + + /** + * Set Shrink to fit. + * + * @return $this + */ + public function setShrinkToFit(bool $shrink): static + { + if ($shrink == '') { + $shrink = false; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['shrinkToFit' => $shrink]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->shrinkToFit = $shrink; + } + + return $this; + } + + /** + * Get indent. + */ + public function getIndent(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getIndent(); + } + + return $this->indent; + } + + /** + * Set indent. + * + * @return $this + */ + public function setIndent(int $indent): static + { + if ($indent > 0) { + if ( + $this->getHorizontal() != self::HORIZONTAL_GENERAL + && $this->getHorizontal() != self::HORIZONTAL_LEFT + && $this->getHorizontal() != self::HORIZONTAL_RIGHT + && $this->getHorizontal() != self::HORIZONTAL_DISTRIBUTED + ) { + $indent = 0; // indent not supported + } + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['indent' => $indent]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->indent = $indent; + } + + return $this; + } + + /** + * Get read order. + */ + public function getReadOrder(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getReadOrder(); + } + + return $this->readOrder; + } + + /** + * Set read order. + * + * @return $this + */ + public function setReadOrder(int $readOrder): static + { + if ($readOrder < 0 || $readOrder > 2) { + $readOrder = 0; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['readOrder' => $readOrder]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->readOrder = $readOrder; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->horizontal + . (($this->justifyLastLine === null) ? 'null' : ($this->justifyLastLine ? 't' : 'f')) + . $this->vertical + . $this->textRotation + . ($this->wrapText ? 't' : 'f') + . ($this->shrinkToFit ? 't' : 'f') + . $this->indent + . $this->readOrder + . __CLASS__ + ); + } + + /** @return mixed[] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'horizontal', $this->getHorizontal()); + $this->exportArray2($exportedArray, 'justifyLastLine', $this->getJustifyLastLine()); + $this->exportArray2($exportedArray, 'indent', $this->getIndent()); + $this->exportArray2($exportedArray, 'readOrder', $this->getReadOrder()); + $this->exportArray2($exportedArray, 'shrinkToFit', $this->getShrinkToFit()); + $this->exportArray2($exportedArray, 'textRotation', $this->getTextRotation()); + $this->exportArray2($exportedArray, 'vertical', $this->getVertical()); + $this->exportArray2($exportedArray, 'wrapText', $this->getWrapText()); + + return $exportedArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Border.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Border.php new file mode 100644 index 0000000..ea2ff5f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Border.php @@ -0,0 +1,228 @@ +color = new Color(Color::COLOR_BLACK, $isSupervisor); + + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->color->bindParent($this, 'color'); + } + if ($isConditional) { + $this->borderStyle = self::BORDER_OMIT; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + + /** @var Borders $sharedComponent */ + $sharedComponent = $parent->getSharedComponent(); + + return match ($this->parentPropertyName) { + 'bottom' => $sharedComponent->getBottom(), + 'diagonal' => $sharedComponent->getDiagonal(), + 'left' => $sharedComponent->getLeft(), + 'right' => $sharedComponent->getRight(), + 'top' => $sharedComponent->getTop(), + default => throw new PhpSpreadsheetException('Cannot get shared component for a pseudo-border.'), + }; + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return mixed[] + */ + public function getStyleArray(array $array): array + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getStyleArray([$this->parentPropertyName => $array]); + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->getTop()->applyFromArray( + * [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ); + * + * + * @param mixed[] $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + /** @var array{borderStyle?: string, color?: array{rgb?: string, argb?: string}} $styleArray */ + if (isset($styleArray['borderStyle'])) { + $this->setBorderStyle($styleArray['borderStyle']); + } + if (isset($styleArray['color'])) { + $this->getColor() + ->applyFromArray($styleArray['color']); + } + } + + return $this; + } + + /** + * Get Border style. + */ + public function getBorderStyle(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBorderStyle(); + } + + return $this->borderStyle; + } + + /** + * Set Border style. + * + * @param bool|string $style When passing a boolean, FALSE equates Border::BORDER_NONE + * and TRUE to Border::BORDER_MEDIUM + * + * @return $this + */ + public function setBorderStyle(bool|string $style): static + { + if (empty($style)) { + $style = self::BORDER_NONE; + } elseif (is_bool($style)) { + $style = self::BORDER_MEDIUM; + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['borderStyle' => $style]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->borderStyle = $style; + } + + return $this; + } + + /** + * Get Border Color. + */ + public function getColor(): Color + { + return $this->color; + } + + /** + * Set Border Color. + * + * @return $this + */ + public function setColor(Color $color): static + { + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->color = $color; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->borderStyle + . $this->color->getHashCode() + . __CLASS__ + ); + } + + /** @return mixed[] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'borderStyle', $this->getBorderStyle()); + $this->exportArray2($exportedArray, 'color', $this->getColor()); + + return $exportedArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Borders.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Borders.php new file mode 100644 index 0000000..05ee0df --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Borders.php @@ -0,0 +1,378 @@ +left = new Border($isSupervisor, $isConditional); + $this->right = new Border($isSupervisor, $isConditional); + $this->top = new Border($isSupervisor, $isConditional); + $this->bottom = new Border($isSupervisor, $isConditional); + $this->diagonal = new Border($isSupervisor, $isConditional); + $this->diagonalDirection = self::DIAGONAL_NONE; + + // Specially for supervisor + if ($isSupervisor) { + // Initialize pseudo-borders + $this->allBorders = new Border(true, $isConditional); + $this->outline = new Border(true, $isConditional); + $this->inside = new Border(true, $isConditional); + $this->vertical = new Border(true, $isConditional); + $this->horizontal = new Border(true, $isConditional); + + // bind parent if we are a supervisor + $this->left->bindParent($this, 'left'); + $this->right->bindParent($this, 'right'); + $this->top->bindParent($this, 'top'); + $this->bottom->bindParent($this, 'bottom'); + $this->diagonal->bindParent($this, 'diagonal'); + $this->allBorders->bindParent($this, 'allBorders'); + $this->outline->bindParent($this, 'outline'); + $this->inside->bindParent($this, 'inside'); + $this->vertical->bindParent($this, 'vertical'); + $this->horizontal->bindParent($this, 'horizontal'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getBorders(); + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return array{borders: mixed[]} + */ + public function getStyleArray(array $array): array + { + return ['borders' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->applyFromArray( + * [ + * 'bottom' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ], + * 'top' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ] + * ); + * + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getBorders()->applyFromArray( + * [ + * 'allBorders' => [ + * 'borderStyle' => Border::BORDER_DASHDOT, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ] + * ); + * + * + * @param mixed[] $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + /** @var array{left?: float[], right?: float[], top?: float[], bottom?: float[], diagonal?: mixed[], diagonalDirection?: int, allBorders?: mixed[][]} $styleArray */ + if (isset($styleArray['left'])) { + $this->getLeft()->applyFromArray($styleArray['left']); + } + if (isset($styleArray['right'])) { + $this->getRight()->applyFromArray($styleArray['right']); + } + if (isset($styleArray['top'])) { + $this->getTop()->applyFromArray($styleArray['top']); + } + if (isset($styleArray['bottom'])) { + $this->getBottom()->applyFromArray($styleArray['bottom']); + } + if (isset($styleArray['diagonal'])) { + $this->getDiagonal()->applyFromArray($styleArray['diagonal']); + } + if (isset($styleArray['diagonalDirection'])) { + $this->setDiagonalDirection($styleArray['diagonalDirection']); + } + if (isset($styleArray['allBorders'])) { + $this->getLeft()->applyFromArray($styleArray['allBorders']); + $this->getRight()->applyFromArray($styleArray['allBorders']); + $this->getTop()->applyFromArray($styleArray['allBorders']); + $this->getBottom()->applyFromArray($styleArray['allBorders']); + } + } + + return $this; + } + + /** + * Get Left. + */ + public function getLeft(): Border + { + return $this->left; + } + + /** + * Get Right. + */ + public function getRight(): Border + { + return $this->right; + } + + /** + * Get Top. + */ + public function getTop(): Border + { + return $this->top; + } + + /** + * Get Bottom. + */ + public function getBottom(): Border + { + return $this->bottom; + } + + /** + * Get Diagonal. + */ + public function getDiagonal(): Border + { + return $this->diagonal; + } + + /** + * Get AllBorders (pseudo-border). Only applies to supervisor. + */ + public function getAllBorders(): Border + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->allBorders; + } + + /** + * Get Outline (pseudo-border). Only applies to supervisor. + */ + public function getOutline(): Border + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->outline; + } + + /** + * Get Inside (pseudo-border). Only applies to supervisor. + */ + public function getInside(): Border + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->inside; + } + + /** + * Get Vertical (pseudo-border). Only applies to supervisor. + */ + public function getVertical(): Border + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->vertical; + } + + /** + * Get Horizontal (pseudo-border). Only applies to supervisor. + */ + public function getHorizontal(): Border + { + if (!$this->isSupervisor) { + throw new PhpSpreadsheetException('Can only get pseudo-border for supervisor.'); + } + + return $this->horizontal; + } + + /** + * Get DiagonalDirection. + */ + public function getDiagonalDirection(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getDiagonalDirection(); + } + + return $this->diagonalDirection; + } + + /** + * Set DiagonalDirection. + * + * @param int $direction see self::DIAGONAL_* + * + * @return $this + */ + public function setDiagonalDirection(int $direction): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['diagonalDirection' => $direction]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->diagonalDirection = $direction; + } + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashcode(); + } + + return md5( + $this->getLeft()->getHashCode() + . $this->getRight()->getHashCode() + . $this->getTop()->getHashCode() + . $this->getBottom()->getHashCode() + . $this->getDiagonal()->getHashCode() + . $this->getDiagonalDirection() + . __CLASS__ + ); + } + + /** @return mixed[][] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'bottom', $this->getBottom()); + $this->exportArray2($exportedArray, 'diagonal', $this->getDiagonal()); + $this->exportArray2($exportedArray, 'diagonalDirection', $this->getDiagonalDirection()); + $this->exportArray2($exportedArray, 'left', $this->getLeft()); + $this->exportArray2($exportedArray, 'right', $this->getRight()); + $this->exportArray2($exportedArray, 'top', $this->getTop()); + /** @var mixed[][] $exportedArray */ + + return $exportedArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Color.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Color.php new file mode 100644 index 0000000..10929bc --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Color.php @@ -0,0 +1,478 @@ + self::COLOR_BLACK, + 'White' => self::COLOR_WHITE, + 'Red' => self::COLOR_RED, + 'Green' => self::COLOR_GREEN, + 'Blue' => self::COLOR_BLUE, + 'Yellow' => self::COLOR_YELLOW, + 'Magenta' => self::COLOR_MAGENTA, + 'Cyan' => self::COLOR_CYAN, + ]; + + const VALIDATE_ARGB_SIZE = 8; + const VALIDATE_RGB_SIZE = 6; + const VALIDATE_COLOR_6 = '/^[A-F0-9]{6}$/i'; + const VALIDATE_COLOR_8 = '/^[A-F0-9]{8}$/i'; + + private const INDEXED_COLORS = [ + 1 => 'FF000000', // System Colour #1 - Black + 2 => 'FFFFFFFF', // System Colour #2 - White + 3 => 'FFFF0000', // System Colour #3 - Red + 4 => 'FF00FF00', // System Colour #4 - Green + 5 => 'FF0000FF', // System Colour #5 - Blue + 6 => 'FFFFFF00', // System Colour #6 - Yellow + 7 => 'FFFF00FF', // System Colour #7- Magenta + 8 => 'FF00FFFF', // System Colour #8- Cyan + 9 => 'FF800000', // Standard Colour #9 + 10 => 'FF008000', // Standard Colour #10 + 11 => 'FF000080', // Standard Colour #11 + 12 => 'FF808000', // Standard Colour #12 + 13 => 'FF800080', // Standard Colour #13 + 14 => 'FF008080', // Standard Colour #14 + 15 => 'FFC0C0C0', // Standard Colour #15 + 16 => 'FF808080', // Standard Colour #16 + 17 => 'FF9999FF', // Chart Fill Colour #17 + 18 => 'FF993366', // Chart Fill Colour #18 + 19 => 'FFFFFFCC', // Chart Fill Colour #19 + 20 => 'FFCCFFFF', // Chart Fill Colour #20 + 21 => 'FF660066', // Chart Fill Colour #21 + 22 => 'FFFF8080', // Chart Fill Colour #22 + 23 => 'FF0066CC', // Chart Fill Colour #23 + 24 => 'FFCCCCFF', // Chart Fill Colour #24 + 25 => 'FF000080', // Chart Line Colour #25 + 26 => 'FFFF00FF', // Chart Line Colour #26 + 27 => 'FFFFFF00', // Chart Line Colour #27 + 28 => 'FF00FFFF', // Chart Line Colour #28 + 29 => 'FF800080', // Chart Line Colour #29 + 30 => 'FF800000', // Chart Line Colour #30 + 31 => 'FF008080', // Chart Line Colour #31 + 32 => 'FF0000FF', // Chart Line Colour #32 + 33 => 'FF00CCFF', // Standard Colour #33 + 34 => 'FFCCFFFF', // Standard Colour #34 + 35 => 'FFCCFFCC', // Standard Colour #35 + 36 => 'FFFFFF99', // Standard Colour #36 + 37 => 'FF99CCFF', // Standard Colour #37 + 38 => 'FFFF99CC', // Standard Colour #38 + 39 => 'FFCC99FF', // Standard Colour #39 + 40 => 'FFFFCC99', // Standard Colour #40 + 41 => 'FF3366FF', // Standard Colour #41 + 42 => 'FF33CCCC', // Standard Colour #42 + 43 => 'FF99CC00', // Standard Colour #43 + 44 => 'FFFFCC00', // Standard Colour #44 + 45 => 'FFFF9900', // Standard Colour #45 + 46 => 'FFFF6600', // Standard Colour #46 + 47 => 'FF666699', // Standard Colour #47 + 48 => 'FF969696', // Standard Colour #48 + 49 => 'FF003366', // Standard Colour #49 + 50 => 'FF339966', // Standard Colour #50 + 51 => 'FF003300', // Standard Colour #51 + 52 => 'FF333300', // Standard Colour #52 + 53 => 'FF993300', // Standard Colour #53 + 54 => 'FF993366', // Standard Colour #54 + 55 => 'FF333399', // Standard Colour #55 + 56 => 'FF333333', // Standard Colour #56 + ]; + + /** + * ARGB - Alpha RGB. + */ + protected ?string $argb = null; + + private bool $hasChanged = false; + + private int $theme = -1; + + /** + * Create a new Color. + * + * @param string $colorValue ARGB value for the colour, or named colour + * @param bool $isSupervisor Flag indicating if this is a supervisor or not + * Leave this value at default unless you understand exactly what + * its ramifications are + * @param bool $isConditional Flag indicating if this is a conditional style or not + * Leave this value at default unless you understand exactly what + * its ramifications are + */ + public function __construct(string $colorValue = self::COLOR_BLACK, bool $isSupervisor = false, bool $isConditional = false) + { + // Supervisor? + parent::__construct($isSupervisor); + + // Initialise values + if (!$isConditional) { + $this->argb = $this->validateColor($colorValue) ?: self::COLOR_BLACK; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + /** @var Border|Fill $sharedComponent */ + $sharedComponent = $parent->getSharedComponent(); + if ($sharedComponent instanceof Fill) { + if ($this->parentPropertyName === 'endColor') { + return $sharedComponent->getEndColor(); + } + + return $sharedComponent->getStartColor(); + } + + return $sharedComponent->getColor(); + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return mixed[] + */ + public function getStyleArray(array $array): array + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getStyleArray([$this->parentPropertyName => $array]); + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->getColor()->applyFromArray(['rgb' => '808080']); + * + * + * @param array{rgb?: string, argb?: string, theme?: int} $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet() + ->getStyle($this->getSelectedCells()) + ->applyFromArray( + $this->getStyleArray($styleArray) + ); + } else { + if (isset($styleArray['rgb'])) { + $this->setRGB($styleArray['rgb']); + } + if (isset($styleArray['argb'])) { + $this->setARGB($styleArray['argb']); + } + if (isset($styleArray['theme'])) { + $this->setTheme($styleArray['theme']); + } + } + + return $this; + } + + private function validateColor(?string $colorValue): string + { + if ($colorValue === null || $colorValue === '') { + return self::COLOR_BLACK; + } + $named = ucfirst(strtolower($colorValue)); + if (array_key_exists($named, self::NAMED_COLOR_TRANSLATIONS)) { + return self::NAMED_COLOR_TRANSLATIONS[$named]; + } + if (preg_match(self::VALIDATE_COLOR_8, $colorValue) === 1) { + return $colorValue; + } + if (preg_match(self::VALIDATE_COLOR_6, $colorValue) === 1) { + return 'FF' . $colorValue; + } + + return ''; + } + + /** + * Get ARGB. + */ + public function getARGB(): ?string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getARGB(); + } + + return $this->argb; + } + + /** + * Set ARGB. + * + * @param ?string $colorValue ARGB value, or a named color + * + * @return $this + */ + public function setARGB(?string $colorValue = self::COLOR_BLACK, bool $nullStringOkay = false): static + { + $this->hasChanged = true; + $this->setTheme(-1); + if (!$nullStringOkay || $colorValue !== '') { + $colorValue = $this->validateColor($colorValue); + if ($colorValue === '') { + return $this; + } + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['argb' => $colorValue]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->argb = $colorValue; + } + + return $this; + } + + /** + * Get RGB. + */ + public function getRGB(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getRGB(); + } + + return substr($this->argb ?? '', 2); + } + + /** + * Set RGB. + * + * @param ?string $colorValue RGB value, or a named color + * + * @return $this + */ + public function setRGB(?string $colorValue = self::COLOR_BLACK): static + { + return $this->setARGB($colorValue); + } + + /** + * Get a specified colour component of an RGB value. + * + * @param string $rgbValue The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param int $offset Position within the RGB value to extract + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return int|string The extracted colour component + */ + private static function getColourComponent(string $rgbValue, int $offset, bool $hex = true): string|int + { + $colour = substr($rgbValue, $offset, 2) ?: ''; + if (preg_match('/^[0-9a-f]{2}$/i', $colour) !== 1) { + $colour = '00'; + } + + return ($hex) ? $colour : (int) hexdec($colour); + } + + /** + * Get the red colour component of an RGB value. + * + * @param string $rgbValue The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return int|string The red colour component + */ + public static function getRed(string $rgbValue, bool $hex = true) + { + return self::getColourComponent($rgbValue, strlen($rgbValue) - 6, $hex); + } + + /** + * Get the green colour component of an RGB value. + * + * @param string $rgbValue The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return int|string The green colour component + */ + public static function getGreen(string $rgbValue, bool $hex = true) + { + return self::getColourComponent($rgbValue, strlen($rgbValue) - 4, $hex); + } + + /** + * Get the blue colour component of an RGB value. + * + * @param string $rgbValue The colour as an RGB value (e.g. FF00CCCC or CCDDEE + * @param bool $hex Flag indicating whether the component should be returned as a hex or a + * decimal value + * + * @return int|string The blue colour component + */ + public static function getBlue(string $rgbValue, bool $hex = true) + { + return self::getColourComponent($rgbValue, strlen($rgbValue) - 2, $hex); + } + + /** + * Adjust the brightness of a color. + * + * @param string $hexColourValue The colour as an RGBA or RGB value (e.g. FF00CCCC or CCDDEE) + * @param float $adjustPercentage The percentage by which to adjust the colour as a float from -1 to 1 + * + * @return string The adjusted colour as an RGBA or RGB value (e.g. FF00CCCC or CCDDEE) + */ + public static function changeBrightness(string $hexColourValue, float $adjustPercentage): string + { + $rgba = (strlen($hexColourValue) === 8); + $adjustPercentage = max(-1.0, min(1.0, $adjustPercentage)); + + /** @var int $red */ + $red = self::getRed($hexColourValue, false); + /** @var int $green */ + $green = self::getGreen($hexColourValue, false); + /** @var int $blue */ + $blue = self::getBlue($hexColourValue, false); + + return (($rgba) ? 'FF' : '') . RgbTint::rgbAndTintToRgb($red, $green, $blue, $adjustPercentage); + } + + /** + * Get indexed color. + * + * @param int $colorIndex Index entry point into the colour array + * @param bool $background Flag to indicate whether default background or foreground colour + * should be returned if the indexed colour doesn't exist + * @param null|string[] $palette + */ + public static function indexedColor(int $colorIndex, bool $background = false, ?array $palette = null): self + { + // Clean parameter + $colorIndex = (int) $colorIndex; + + if (empty($palette)) { + if (isset(self::INDEXED_COLORS[$colorIndex])) { + return new self(self::INDEXED_COLORS[$colorIndex]); + } + } else { + if (isset($palette[$colorIndex])) { + return new self($palette[$colorIndex]); + } + } + + return ($background) ? new self(self::COLOR_WHITE) : new self(self::COLOR_BLACK); + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->argb + . (string) $this->theme + . __CLASS__ + ); + } + + /** @return mixed[] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'argb', $this->getARGB()); + $this->exportArray2($exportedArray, 'theme', $this->getTheme()); + + return $exportedArray; + } + + public function getHasChanged(): bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->hasChanged; + } + + return $this->hasChanged; + } + + public function getTheme(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getTheme(); + } + + return $this->theme; + } + + public function setTheme(int $theme): self + { + $this->hasChanged = true; + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['theme' => $theme]); + $this->getActiveSheet() + ->getStyle($this->getSelectedCells()) + ->applyFromArray($styleArray); + } else { + $this->theme = $theme; + } + + return $this; + } + + public function setHyperlinkTheme(): self + { + $rgb = $this->getActiveSheet() + ->getParent() + ?->getTheme() + ->getThemeColors(); + if (is_array($rgb) && array_key_exists('hlink', $rgb)) { + $this->setRGB($rgb['hlink']); + } + + return $this->setTheme(Theme::HYPERLINK_THEME); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Conditional.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Conditional.php new file mode 100644 index 0000000..fedd0aa --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Conditional.php @@ -0,0 +1,376 @@ +style = new Style(false, true); + } + + public function getPriority(): int + { + return $this->priority; + } + + public function setPriority(int $priority): self + { + $this->priority = $priority; + + return $this; + } + + public function getNoFormatSet(): bool + { + return $this->noFormatSet; + } + + public function setNoFormatSet(bool $noFormatSet): self + { + $this->noFormatSet = $noFormatSet; + + return $this; + } + + /** + * Get Condition type. + */ + public function getConditionType(): string + { + return $this->conditionType; + } + + /** + * Set Condition type. + * + * @param string $type Condition type, see self::CONDITION_* + * + * @return $this + */ + public function setConditionType(string $type): static + { + $this->conditionType = $type; + + return $this; + } + + /** + * Get Operator type. + */ + public function getOperatorType(): string + { + return $this->operatorType; + } + + /** + * Set Operator type. + * + * @param string $type Conditional operator type, see self::OPERATOR_* + * + * @return $this + */ + public function setOperatorType(string $type): static + { + $this->operatorType = $type; + + return $this; + } + + /** + * Get text. + */ + public function getText(): string + { + return $this->text; + } + + /** + * Set text. + * + * @return $this + */ + public function setText(string $text): static + { + $this->text = $text; + + return $this; + } + + /** + * Get StopIfTrue. + */ + public function getStopIfTrue(): bool + { + return $this->stopIfTrue; + } + + /** + * Set StopIfTrue. + * + * @return $this + */ + public function setStopIfTrue(bool $stopIfTrue): static + { + $this->stopIfTrue = $stopIfTrue; + + return $this; + } + + /** + * Get Conditions. + * + * @return (bool|float|int|string)[] + */ + public function getConditions(): array + { + return $this->condition; + } + + /** + * Set Conditions. + * + * @param bool|(bool|float|int|string)[]|float|int|string $conditions Condition + * + * @return $this + */ + public function setConditions($conditions): static + { + if (!is_array($conditions)) { + $conditions = [$conditions]; + } + $this->condition = $conditions; + + return $this; + } + + /** + * Add Condition. + * + * @param bool|float|int|string $condition Condition + * + * @return $this + */ + public function addCondition($condition): static + { + $this->condition[] = $condition; + + return $this; + } + + /** + * Get Style. + */ + public function getStyle(mixed $cellData = null): Style + { + if ($this->conditionType === self::CONDITION_COLORSCALE && $cellData !== null && $this->colorScale !== null && is_numeric($cellData)) { + $style = new Style(isConditional: true); + $style->getFill()->setFillType(Fill::FILL_SOLID); + $style->getFill()->getStartColor()->setARGB($this->colorScale->getColorForValue((float) $cellData)); + + return $style; + } + + return $this->style; + } + + /** + * Set Style. + * + * @return $this + */ + public function setStyle(Style $style): static + { + $this->style = $style; + + return $this; + } + + public function getDataBar(): ?ConditionalDataBar + { + return $this->dataBar; + } + + public function setDataBar(ConditionalDataBar $dataBar): static + { + $this->dataBar = $dataBar; + + return $this; + } + + public function getColorScale(): ?ConditionalColorScale + { + return $this->colorScale; + } + + public function setColorScale(ConditionalColorScale $colorScale): static + { + $this->colorScale = $colorScale; + + return $this; + } + + public function getIconSet(): ?ConditionalIconSet + { + return $this->iconSet; + } + + public function setIconSet(ConditionalIconSet $iconSet): static + { + $this->iconSet = $iconSet; + + return $this; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + return md5( + $this->conditionType + . $this->operatorType + . implode(';', $this->condition) + . $this->style->getHashCode() + . __CLASS__ + ); + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + $this->$key = clone $value; + } else { + $this->$key = $value; + } + } + } + + /** + * Verify if param is valid condition type. + */ + public static function isValidConditionType(string $type): bool + { + return in_array($type, self::CONDITION_TYPES); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php new file mode 100644 index 0000000..f0deb56 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php @@ -0,0 +1,297 @@ + '=', + Conditional::OPERATOR_GREATERTHAN => '>', + Conditional::OPERATOR_GREATERTHANOREQUAL => '>=', + Conditional::OPERATOR_LESSTHAN => '<', + Conditional::OPERATOR_LESSTHANOREQUAL => '<=', + Conditional::OPERATOR_NOTEQUAL => '<>', + ]; + + public const COMPARISON_RANGE_OPERATORS = [ + Conditional::OPERATOR_BETWEEN => 'IF(AND(A1>=%s,A1<=%s),TRUE,FALSE)', + Conditional::OPERATOR_NOTBETWEEN => 'IF(AND(A1>=%s,A1<=%s),FALSE,TRUE)', + ]; + + public const COMPARISON_DUPLICATES_OPERATORS = [ + Conditional::CONDITION_DUPLICATES => "COUNTIF('%s'!%s,%s)>1", + Conditional::CONDITION_UNIQUE => "COUNTIF('%s'!%s,%s)=1", + ]; + + protected Cell $cell; + + protected int $cellRow; + + protected Worksheet $worksheet; + + protected int $cellColumn; + + protected string $conditionalRange; + + protected string $referenceCell; + + protected int $referenceRow; + + protected int $referenceColumn; + + protected Calculation $engine; + + public function __construct(Cell $cell, string $conditionalRange) + { + $this->cell = $cell; + $this->worksheet = $cell->getWorksheet(); + [$this->cellColumn, $this->cellRow] = Coordinate::indexesFromString($this->cell->getCoordinate()); + $this->setReferenceCellForExpressions($conditionalRange); + + $this->engine = Calculation::getInstance($this->worksheet->getParent()); + } + + protected function setReferenceCellForExpressions(string $conditionalRange): void + { + $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange))); + [$this->referenceCell] = $conditionalRange[0]; + + [$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell); + + // Convert our conditional range to an absolute conditional range, so it can be used "pinned" in formulae + $rangeSets = []; + foreach ($conditionalRange as $rangeSet) { + $absoluteRangeSet = array_map( + [Coordinate::class, 'absoluteCoordinate'], + $rangeSet + ); + $rangeSets[] = implode(':', $absoluteRangeSet); + } + $this->conditionalRange = implode(',', $rangeSets); + } + + public function evaluateConditional(Conditional $conditional): bool + { + // Some calculations may modify the stored cell; so reset it before every evaluation. + $cellColumn = Coordinate::stringFromColumnIndex($this->cellColumn); + $cellAddress = "{$cellColumn}{$this->cellRow}"; + $this->cell = $this->worksheet->getCell($cellAddress); + + return match ($conditional->getConditionType()) { + Conditional::CONDITION_CELLIS => $this->processOperatorComparison($conditional), + Conditional::CONDITION_DUPLICATES, Conditional::CONDITION_UNIQUE => $this->processDuplicatesComparison($conditional), + // Expression is NOT(ISERROR(SEARCH("",))) + Conditional::CONDITION_CONTAINSTEXT, + // Expression is ISERROR(SEARCH("",)) + Conditional::CONDITION_NOTCONTAINSTEXT, + // Expression is LEFT(,LEN(""))="" + Conditional::CONDITION_BEGINSWITH, + // Expression is RIGHT(,LEN(""))="" + Conditional::CONDITION_ENDSWITH, + // Expression is LEN(TRIM())=0 + Conditional::CONDITION_CONTAINSBLANKS, + // Expression is LEN(TRIM())>0 + Conditional::CONDITION_NOTCONTAINSBLANKS, + // Expression is ISERROR() + Conditional::CONDITION_CONTAINSERRORS, + // Expression is NOT(ISERROR()) + Conditional::CONDITION_NOTCONTAINSERRORS, + // Expression varies, depending on specified timePeriod value, e.g. + // Yesterday FLOOR(,1)=TODAY()-1 + // Today FLOOR(,1)=TODAY() + // Tomorrow FLOOR(,1)=TODAY()+1 + // Last 7 Days AND(TODAY()-FLOOR(,1)<=6,FLOOR(,1)<=TODAY()) + Conditional::CONDITION_TIMEPERIOD, + Conditional::CONDITION_EXPRESSION => $this->processExpression($conditional), + Conditional::CONDITION_COLORSCALE => $this->processColorScale($conditional), + default => false, + }; + } + + protected function wrapValue(mixed $value): float|int|string + { + if (!is_numeric($value)) { + if (is_bool($value)) { + return $value ? 'TRUE' : 'FALSE'; + } elseif ($value === null) { + return 'NULL'; + } + + return '"' . StringHelper::convertToString($value) . '"'; + } + + return $value; + } + + protected function wrapCellValue(): float|int|string + { + $this->cell = $this->worksheet->getCell([$this->cellColumn, $this->cellRow]); + + return $this->wrapValue($this->cell->getCalculatedValue()); + } + + /** @param string[] $matches */ + protected function conditionCellAdjustment(array $matches): float|int|string + { + $column = $matches[6]; + $row = $matches[7]; + if (!str_contains($column, '$')) { + // $column = Coordinate::stringFromColumnIndex($this->cellColumn); + $column = Coordinate::columnIndexFromString($column); + $column += $this->cellColumn - $this->referenceColumn; + $column = Coordinate::stringFromColumnIndex($column); + } + + if (!str_contains($row, '$')) { + $row = (int) $row + $this->cellRow - $this->referenceRow; + } + + if (!empty($matches[4])) { + $worksheet = $this->worksheet->getParentOrThrow()->getSheetByName(trim($matches[4], "'")); + if ($worksheet === null) { + return $this->wrapValue(null); + } + + return $this->wrapValue( + $worksheet + ->getCell(str_replace('$', '', "{$column}{$row}")) + ->getCalculatedValue() + ); + } + + return $this->wrapValue( + $this->worksheet + ->getCell(str_replace('$', '', "{$column}{$row}")) + ->getCalculatedValue() + ); + } + + protected function cellConditionCheck(string $condition): string + { + $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition); + $i = false; + foreach ($splitCondition as &$value) { + // Only count/replace in alternating array entries (ie. not in quoted strings) + $i = $i === false; + if ($i) { + $value = (string) preg_replace_callback( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', + [$this, 'conditionCellAdjustment'], + $value + ); + } + } + unset($value); + + // Then rebuild the condition string to return it + return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition); + } + + /** + * @param mixed[] $conditions + * + * @return mixed[] + */ + protected function adjustConditionsForCellReferences(array $conditions): array + { + return array_map( + [$this, 'cellConditionCheck'], + $conditions + ); + } + + protected function processOperatorComparison(Conditional $conditional): bool + { + if (array_key_exists($conditional->getOperatorType(), self::COMPARISON_RANGE_OPERATORS)) { + return $this->processRangeOperator($conditional); + } + + $operator = self::COMPARISON_OPERATORS[$conditional->getOperatorType()]; + $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); + /** @var float|int|string */ + $temp1 = $this->wrapCellValue(); + /** @var scalar */ + $temp2 = array_pop($conditions); + $expression = sprintf('%s%s%s', (string) $temp1, $operator, (string) $temp2); + + return $this->evaluateExpression($expression); + } + + protected function processColorScale(Conditional $conditional): bool + { + if (is_numeric($this->wrapCellValue()) && $conditional->getColorScale()?->colorScaleReadyForUse()) { + return true; + } + + return false; + } + + protected function processRangeOperator(Conditional $conditional): bool + { + $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); + sort($conditions); + $expression = sprintf( + (string) preg_replace( + '/\bA1\b/i', + (string) $this->wrapCellValue(), + self::COMPARISON_RANGE_OPERATORS[$conditional->getOperatorType()] + ), + ...$conditions //* @phpstan-ignore-line + ); + + return $this->evaluateExpression($expression); + } + + protected function processDuplicatesComparison(Conditional $conditional): bool + { + $worksheetName = $this->cell->getWorksheet()->getTitle(); + + $expression = sprintf( + self::COMPARISON_DUPLICATES_OPERATORS[$conditional->getConditionType()], + $worksheetName, + $this->conditionalRange, + $this->cellConditionCheck($this->cell->getCalculatedValueString()) + ); + + return $this->evaluateExpression($expression); + } + + protected function processExpression(Conditional $conditional): bool + { + $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); + /** @var string */ + $expression = array_pop($conditions); + /** @var float|int|string */ + $temp = $this->wrapCellValue(); + + $expression = (string) preg_replace( + '/\b' . $this->referenceCell . '\b/i', + (string) $temp, + $expression + ); + + return $this->evaluateExpression($expression); + } + + protected function evaluateExpression(string $expression): bool + { + $expression = "={$expression}"; + + try { + $this->engine->flushInstance(); + $result = (bool) $this->engine->calculateFormula($expression); + } catch (Exception) { + return false; + } + + return $result; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php new file mode 100644 index 0000000..f8826f0 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php @@ -0,0 +1,65 @@ +cell = $cell; + $this->cellMatcher = new CellMatcher($cell, $conditionalRange); + $this->styleMerger = new StyleMerger($cell->getStyle()); + } + + /** + * @param Conditional[] $conditionalStyles + */ + public function matchConditions(array $conditionalStyles = []): Style + { + foreach ($conditionalStyles as $conditional) { + if ($this->cellMatcher->evaluateConditional($conditional) === true) { + // Merging the conditional style into the base style goes in here + $this->styleMerger->mergeStyle($conditional->getStyle($this->cell->getValue())); + if ($conditional->getStopIfTrue() === true) { + break; + } + } + } + + return $this->styleMerger->getStyle(); + } + + /** + * @param Conditional[] $conditionalStyles + */ + public function matchConditionsReturnNullIfNoneMatched(array $conditionalStyles, string $cellData, bool $stopAtFirstMatch = false): ?Style + { + $matched = false; + $value = (float) $cellData; + foreach ($conditionalStyles as $conditional) { + if ($this->cellMatcher->evaluateConditional($conditional) === true) { + $matched = true; + // Merging the conditional style into the base style goes in here + $this->styleMerger->mergeStyle($conditional->getStyle($value)); + if ($conditional->getStopIfTrue() === true || $stopAtFirstMatch) { + break; + } + } + } + if ($matched) { + return $this->styleMerger->getStyle(); + } + + return null; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php new file mode 100644 index 0000000..539b331 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php @@ -0,0 +1,268 @@ +minimumConditionalFormatValueObject; + } + + public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject): self + { + $this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject; + + return $this; + } + + public function getMidpointConditionalFormatValueObject(): ?ConditionalFormatValueObject + { + return $this->midpointConditionalFormatValueObject; + } + + public function setMidpointConditionalFormatValueObject(ConditionalFormatValueObject $midpointConditionalFormatValueObject): self + { + $this->midpointConditionalFormatValueObject = $midpointConditionalFormatValueObject; + + return $this; + } + + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject + { + return $this->maximumConditionalFormatValueObject; + } + + public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject): self + { + $this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject; + + return $this; + } + + public function getMinimumColor(): ?Color + { + return $this->minimumColor; + } + + public function setMinimumColor(Color $minimumColor): self + { + $this->minimumColor = $minimumColor; + + return $this; + } + + public function getMidpointColor(): ?Color + { + return $this->midpointColor; + } + + public function setMidpointColor(Color $midpointColor): self + { + $this->midpointColor = $midpointColor; + + return $this; + } + + public function getMaximumColor(): ?Color + { + return $this->maximumColor; + } + + public function setMaximumColor(Color $maximumColor): self + { + $this->maximumColor = $maximumColor; + + return $this; + } + + public function getSqRef(): ?string + { + return $this->sqref; + } + + public function setSqRef(string $sqref, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): self + { + $this->sqref = $sqref; + $this->worksheet = $worksheet; + + return $this; + } + + public function setScaleArray(): self + { + if ($this->sqref !== null && $this->worksheet !== null) { + $values = $this->worksheet->rangesToArray($this->sqref, null, true, true, true); + $this->valueArray = []; + foreach ($values as $key => $value) { + /** @var array $value */ + foreach ($value as $k => $v) { + $this->valueArray[] = (float) $v; + } + } + $this->prepareColorScale(); + } + + return $this; + } + + public function getColorForValue(float $value): string + { + if ($this->minimumColor === null || $this->midpointColor === null || $this->maximumColor === null) { + return 'FF000000'; + } + $minColor = $this->minimumColor->getARGB(); + $midColor = $this->midpointColor->getARGB(); + $maxColor = $this->maximumColor->getARGB(); + + if ($minColor === null || $midColor === null || $maxColor === null) { + return 'FF000000'; + } + + if ($value <= $this->minValue) { + return $minColor; + } + if ($value >= $this->maxValue) { + return $maxColor; + } + if ($value == $this->midValue) { + return $midColor; + } + if ($value < $this->midValue) { + $blend = ($value - $this->minValue) / ($this->midValue - $this->minValue); + $alpha1 = hexdec(substr($minColor, 0, 2)); + $alpha2 = hexdec(substr($midColor, 0, 2)); + $red1 = hexdec(substr($minColor, 2, 2)); + $red2 = hexdec(substr($midColor, 2, 2)); + $green1 = hexdec(substr($minColor, 4, 2)); + $green2 = hexdec(substr($midColor, 4, 2)); + $blue1 = hexdec(substr($minColor, 6, 2)); + $blue2 = hexdec(substr($midColor, 6, 2)); + + return strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend)))); + } + $blend = ($value - $this->midValue) / ($this->maxValue - $this->midValue); + $alpha1 = hexdec(substr($midColor, 0, 2)); + $alpha2 = hexdec(substr($maxColor, 0, 2)); + $red1 = hexdec(substr($midColor, 2, 2)); + $red2 = hexdec(substr($maxColor, 2, 2)); + $green1 = hexdec(substr($midColor, 4, 2)); + $green2 = hexdec(substr($maxColor, 4, 2)); + $blue1 = hexdec(substr($midColor, 6, 2)); + $blue2 = hexdec(substr($maxColor, 6, 2)); + + return strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend)))); + } + + private function getLimitValue(string $type, float $value = 0, float $formula = 0): float + { + if (count($this->valueArray) === 0) { + return 0; + } + switch ($type) { + case 'min': + /** @var float|int */ + $temp = min($this->valueArray); + + return (float) $temp; + case 'max': + /** @var float|int */ + $temp = max($this->valueArray); + + return (float) $temp; + case 'percentile': + return (float) Percentiles::PERCENTILE($this->valueArray, (float) ($value / 100)); + case 'formula': + return $formula; + case 'percent': + /** @var float|int */ + $min = min($this->valueArray); + $min = (float) $min; + /** @var float|int */ + $max = max($this->valueArray); + $max = (float) $max; + + return $min + (float) ($value / 100) * ($max - $min); + default: + return 0; + } + } + + /** + * Prepares color scale for execution, see the first if for variables that must be set beforehand. + */ + public function prepareColorScale(): self + { + if ($this->minimumConditionalFormatValueObject !== null && $this->maximumConditionalFormatValueObject !== null && $this->minimumColor !== null && $this->maximumColor !== null) { + if ($this->midpointConditionalFormatValueObject !== null && $this->midpointConditionalFormatValueObject->getType() !== 'None') { + $this->minValue = $this->getLimitValue($this->minimumConditionalFormatValueObject->getType(), (float) $this->minimumConditionalFormatValueObject->getValue(), (float) $this->minimumConditionalFormatValueObject->getCellFormula()); + $this->midValue = $this->getLimitValue($this->midpointConditionalFormatValueObject->getType(), (float) $this->midpointConditionalFormatValueObject->getValue(), (float) $this->midpointConditionalFormatValueObject->getCellFormula()); + $this->maxValue = $this->getLimitValue($this->maximumConditionalFormatValueObject->getType(), (float) $this->maximumConditionalFormatValueObject->getValue(), (float) $this->maximumConditionalFormatValueObject->getCellFormula()); + } else { + $this->minValue = $this->getLimitValue($this->minimumConditionalFormatValueObject->getType(), (float) $this->minimumConditionalFormatValueObject->getValue(), (float) $this->minimumConditionalFormatValueObject->getCellFormula()); + $this->maxValue = $this->getLimitValue($this->maximumConditionalFormatValueObject->getType(), (float) $this->maximumConditionalFormatValueObject->getValue(), (float) $this->maximumConditionalFormatValueObject->getCellFormula()); + $this->midValue = (float) ($this->minValue + $this->maxValue) / 2; + $blend = 0.5; + + $minColor = $this->minimumColor->getARGB(); + $maxColor = $this->maximumColor->getARGB(); + + if ($minColor !== null && $maxColor !== null) { + $alpha1 = hexdec(substr($minColor, 0, 2)); + $alpha2 = hexdec(substr($maxColor, 0, 2)); + $red1 = hexdec(substr($minColor, 2, 2)); + $red2 = hexdec(substr($maxColor, 2, 2)); + $green1 = hexdec(substr($minColor, 4, 2)); + $green2 = hexdec(substr($maxColor, 4, 2)); + $blue1 = hexdec(substr($minColor, 6, 2)); + $blue2 = hexdec(substr($maxColor, 6, 2)); + $this->midpointColor = new Color(strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend))))); + } else { + $this->midpointColor = null; + } + } + } + + return $this; + } + + /** + * Checks that all needed color scale data is in place. + */ + public function colorScaleReadyForUse(): bool + { + if ($this->minimumColor === null || $this->midpointColor === null || $this->maximumColor === null) { + return false; + } + + return true; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php new file mode 100644 index 0000000..370f102 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -0,0 +1,76 @@ +showValue; + } + + public function setShowValue(bool $showValue): self + { + $this->showValue = $showValue; + + return $this; + } + + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject + { + return $this->minimumConditionalFormatValueObject; + } + + public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject): self + { + $this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject; + + return $this; + } + + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject + { + return $this->maximumConditionalFormatValueObject; + } + + public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject): self + { + $this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject; + + return $this; + } + + public function getColor(): string + { + return $this->color; + } + + public function setColor(string $color): self + { + $this->color = $color; + + return $this; + } + + public function getConditionalFormattingRuleExt(): ?ConditionalFormattingRuleExtension + { + return $this->conditionalFormattingRuleExt; + } + + public function setConditionalFormattingRuleExt(ConditionalFormattingRuleExtension $conditionalFormattingRuleExt): self + { + $this->conditionalFormattingRuleExt = $conditionalFormattingRuleExt; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php new file mode 100644 index 0000000..a94bc28 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBarExtension.php @@ -0,0 +1,239 @@ + attributes */ + private int $minLength; + + private int $maxLength; + + private ?bool $border = null; + + private ?bool $gradient = null; + + private ?string $direction = null; + + private ?bool $negativeBarBorderColorSameAsPositive = null; + + private ?string $axisPosition = null; + + // children + + private ConditionalFormatValueObject $maximumConditionalFormatValueObject; + + private ConditionalFormatValueObject $minimumConditionalFormatValueObject; + + private ?string $borderColor = null; + + private ?string $negativeFillColor = null; + + private ?string $negativeBorderColor = null; + + /** @var array{rgb: ?string, theme: ?string, tint: ?string} */ + private array $axisColor = [ + 'rgb' => null, + 'theme' => null, + 'tint' => null, + ]; + + /** @return mixed[] */ + public function getXmlAttributes(): array + { + $ret = []; + foreach (['minLength', 'maxLength', 'direction', 'axisPosition'] as $attrKey) { + if (null !== $this->{$attrKey}) { + $ret[$attrKey] = $this->{$attrKey}; + } + } + foreach (['border', 'gradient', 'negativeBarBorderColorSameAsPositive'] as $attrKey) { + if (null !== $this->{$attrKey}) { + $ret[$attrKey] = $this->{$attrKey} ? '1' : '0'; + } + } + + return $ret; + } + + /** @return mixed[] */ + public function getXmlElements(): array + { + $ret = []; + $elms = ['borderColor', 'negativeFillColor', 'negativeBorderColor']; + foreach ($elms as $elmKey) { + if (null !== $this->{$elmKey}) { + $ret[$elmKey] = ['rgb' => $this->{$elmKey}]; + } + } + foreach (array_filter($this->axisColor) as $attrKey => $axisColorAttr) { + if (!isset($ret['axisColor'])) { + $ret['axisColor'] = []; + } + $ret['axisColor'][$attrKey] = $axisColorAttr; + } + + return $ret; + } + + public function getMinLength(): int + { + return $this->minLength; + } + + public function setMinLength(int $minLength): self + { + $this->minLength = $minLength; + + return $this; + } + + public function getMaxLength(): int + { + return $this->maxLength; + } + + public function setMaxLength(int $maxLength): self + { + $this->maxLength = $maxLength; + + return $this; + } + + public function getBorder(): ?bool + { + return $this->border; + } + + public function setBorder(bool $border): self + { + $this->border = $border; + + return $this; + } + + public function getGradient(): ?bool + { + return $this->gradient; + } + + public function setGradient(bool $gradient): self + { + $this->gradient = $gradient; + + return $this; + } + + public function getDirection(): ?string + { + return $this->direction; + } + + public function setDirection(string $direction): self + { + $this->direction = $direction; + + return $this; + } + + public function getNegativeBarBorderColorSameAsPositive(): ?bool + { + return $this->negativeBarBorderColorSameAsPositive; + } + + public function setNegativeBarBorderColorSameAsPositive(bool $negativeBarBorderColorSameAsPositive): self + { + $this->negativeBarBorderColorSameAsPositive = $negativeBarBorderColorSameAsPositive; + + return $this; + } + + public function getAxisPosition(): ?string + { + return $this->axisPosition; + } + + public function setAxisPosition(string $axisPosition): self + { + $this->axisPosition = $axisPosition; + + return $this; + } + + public function getMaximumConditionalFormatValueObject(): ConditionalFormatValueObject + { + return $this->maximumConditionalFormatValueObject; + } + + public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject): self + { + $this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject; + + return $this; + } + + public function getMinimumConditionalFormatValueObject(): ConditionalFormatValueObject + { + return $this->minimumConditionalFormatValueObject; + } + + public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject): self + { + $this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject; + + return $this; + } + + public function getBorderColor(): ?string + { + return $this->borderColor; + } + + public function setBorderColor(string $borderColor): self + { + $this->borderColor = $borderColor; + + return $this; + } + + public function getNegativeFillColor(): ?string + { + return $this->negativeFillColor; + } + + public function setNegativeFillColor(string $negativeFillColor): self + { + $this->negativeFillColor = $negativeFillColor; + + return $this; + } + + public function getNegativeBorderColor(): ?string + { + return $this->negativeBorderColor; + } + + public function setNegativeBorderColor(string $negativeBorderColor): self + { + $this->negativeBorderColor = $negativeBorderColor; + + return $this; + } + + /** @return array{rgb: ?string, theme: ?string, tint: ?string} */ + public function getAxisColor(): array + { + return $this->axisColor; + } + + public function setAxisColor(?string $rgb, ?string $theme = null, ?string $tint = null): self + { + $this->axisColor = [ + 'rgb' => $rgb, + 'theme' => $theme, + 'tint' => $tint, + ]; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php new file mode 100644 index 0000000..d1dfb51 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php @@ -0,0 +1,74 @@ +type = $type; + $this->value = $value; + $this->cellFormula = $cellFormula; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getValue(): null|float|int|string + { + return $this->value; + } + + public function setValue(null|float|int|string $value): self + { + $this->value = $value; + + return $this; + } + + public function getCellFormula(): ?string + { + return $this->cellFormula; + } + + public function setCellFormula(?string $cellFormula): self + { + $this->cellFormula = $cellFormula; + + return $this; + } + + public function getGreaterThanOrEqual(): ?bool + { + return $this->greaterThanOrEqual; + } + + public function setGreaterThanOrEqual(?bool $greaterThanOrEqual): self + { + $this->greaterThanOrEqual = $greaterThanOrEqual; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php new file mode 100644 index 0000000..906006b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php @@ -0,0 +1,214 @@ +id = '{' . $this->generateUuid() . '}'; + } else { + $this->id = $id; + } + $this->cfRule = $cfRule; + } + + private function generateUuid(): string + { + $chars = mb_str_split('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx', 1, 'UTF-8'); + + foreach ($chars as $i => $char) { + if ($char === 'x') { + $chars[$i] = dechex(random_int(0, 15)); + } elseif ($char === 'y') { + $chars[$i] = dechex(random_int(8, 11)); + } + } + + return implode('', $chars); + } + + /** @return mixed[] */ + public static function parseExtLstXml(?SimpleXMLElement $extLstXml): array + { + $conditionalFormattingRuleExtensions = []; + $conditionalFormattingRuleExtensionXml = null; + if ($extLstXml instanceof SimpleXMLElement) { + foreach ((count($extLstXml) > 0 ? $extLstXml : [$extLstXml]) as $extLst) { + //this uri is conditionalFormattings + //https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627 + if (isset($extLst->ext['uri']) && (string) $extLst->ext['uri'] === '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') { + $conditionalFormattingRuleExtensionXml = $extLst->ext; + } + } + + if ($conditionalFormattingRuleExtensionXml) { + $ns = $conditionalFormattingRuleExtensionXml->getNamespaces(true); + $extFormattingsXml = $conditionalFormattingRuleExtensionXml->children($ns['x14']); + + foreach ($extFormattingsXml->children($ns['x14']) as $extFormattingXml) { + $extCfRuleXml = $extFormattingXml->cfRule; + $attributes = $extCfRuleXml->attributes(); + if (!$attributes || ((string) $attributes->type) !== Conditional::CONDITION_DATABAR) { + continue; + } + + $extFormattingRuleObj = new self((string) $attributes->id); + $extFormattingRuleObj->setSqref((string) $extFormattingXml->children($ns['xm'])->sqref); + $conditionalFormattingRuleExtensions[$extFormattingRuleObj->getId()] = $extFormattingRuleObj; + + $extDataBarObj = new ConditionalDataBarExtension(); + $extFormattingRuleObj->setDataBarExt($extDataBarObj); + $dataBarXml = $extCfRuleXml->dataBar; + self::parseExtDataBarAttributesFromXml($extDataBarObj, $dataBarXml); + self::parseExtDataBarElementChildrenFromXml($extDataBarObj, $dataBarXml, $ns); + } + } + } + + return $conditionalFormattingRuleExtensions; + } + + private static function parseExtDataBarAttributesFromXml( + ConditionalDataBarExtension $extDataBarObj, + SimpleXMLElement $dataBarXml + ): void { + $dataBarAttribute = $dataBarXml->attributes(); + if ($dataBarAttribute === null) { + return; + } + if ($dataBarAttribute->minLength) { + $extDataBarObj->setMinLength((int) $dataBarAttribute->minLength); + } + if ($dataBarAttribute->maxLength) { + $extDataBarObj->setMaxLength((int) $dataBarAttribute->maxLength); + } + if ($dataBarAttribute->border) { + $extDataBarObj->setBorder((bool) (string) $dataBarAttribute->border); + } + if ($dataBarAttribute->gradient) { + $extDataBarObj->setGradient((bool) (string) $dataBarAttribute->gradient); + } + if ($dataBarAttribute->direction) { + $extDataBarObj->setDirection((string) $dataBarAttribute->direction); + } + if ($dataBarAttribute->negativeBarBorderColorSameAsPositive) { + $extDataBarObj->setNegativeBarBorderColorSameAsPositive((bool) (string) $dataBarAttribute->negativeBarBorderColorSameAsPositive); + } + if ($dataBarAttribute->axisPosition) { + $extDataBarObj->setAxisPosition((string) $dataBarAttribute->axisPosition); + } + } + + /** @param string[] $ns */ + private static function parseExtDataBarElementChildrenFromXml(ConditionalDataBarExtension $extDataBarObj, SimpleXMLElement $dataBarXml, array $ns): void + { + if ($dataBarXml->borderColor) { + $attributes = $dataBarXml->borderColor->attributes(); + if ($attributes !== null) { + $extDataBarObj->setBorderColor((string) $attributes['rgb']); + } + } + if ($dataBarXml->negativeFillColor) { + $attributes = $dataBarXml->negativeFillColor->attributes(); + if ($attributes !== null) { + $extDataBarObj->setNegativeFillColor((string) $attributes['rgb']); + } + } + if ($dataBarXml->negativeBorderColor) { + $attributes = $dataBarXml->negativeBorderColor->attributes(); + if ($attributes !== null) { + $extDataBarObj->setNegativeBorderColor((string) $attributes['rgb']); + } + } + if ($dataBarXml->axisColor) { + $axisColorAttr = $dataBarXml->axisColor->attributes(); + if ($axisColorAttr !== null) { + $extDataBarObj->setAxisColor((string) $axisColorAttr['rgb'], (string) $axisColorAttr['theme'], (string) $axisColorAttr['tint']); + } + } + $cfvoIndex = 0; + foreach ($dataBarXml->cfvo as $cfvo) { + $f = (string) $cfvo->children($ns['xm'])->f; + $attributes = $cfvo->attributes(); + if (!($attributes)) { + continue; + } + + if ($cfvoIndex === 0) { + $extDataBarObj->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $attributes['type'], null, (empty($f) ? null : $f))); + } + if ($cfvoIndex === 1) { + $extDataBarObj->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $attributes['type'], null, (empty($f) ? null : $f))); + } + ++$cfvoIndex; + } + } + + public function getId(): string + { + return $this->id; + } + + public function setId(string $id): self + { + $this->id = $id; + + return $this; + } + + public function getCfRule(): string + { + return $this->cfRule; + } + + public function setCfRule(string $cfRule): self + { + $this->cfRule = $cfRule; + + return $this; + } + + public function getDataBarExt(): ConditionalDataBarExtension + { + return $this->dataBar; + } + + public function setDataBarExt(ConditionalDataBarExtension $dataBar): self + { + $this->dataBar = $dataBar; + + return $this; + } + + public function getSqref(): string + { + return $this->sqref; + } + + public function setSqref(string $sqref): self + { + $this->sqref = $sqref; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php new file mode 100644 index 0000000..b7c21d8 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php @@ -0,0 +1,96 @@ +iconSetType; + } + + public function setIconSetType(IconSetValues $type): self + { + $this->iconSetType = $type; + + return $this; + } + + public function getReverse(): ?bool + { + return $this->reverse; + } + + public function setReverse(bool $reverse): self + { + $this->reverse = $reverse; + + return $this; + } + + public function getShowValue(): ?bool + { + return $this->showValue; + } + + public function setShowValue(bool $showValue): self + { + $this->showValue = $showValue; + + return $this; + } + + public function getCustom(): ?bool + { + return $this->custom; + } + + public function setCustom(bool $custom): self + { + $this->custom = $custom; + + return $this; + } + + /** + * Get the conditional format value objects. + * + * @return ConditionalFormatValueObject[] + */ + public function getCfvos(): array + { + return $this->cfvos; + } + + /** + * Set the conditional format value objects. + * + * @param ConditionalFormatValueObject[] $cfvos + */ + public function setCfvos(array $cfvos): self + { + $this->cfvos = $cfvos; + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php new file mode 100644 index 0000000..c27d36e --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php @@ -0,0 +1,28 @@ +exportArray(); + $this->baseStyle = new Style(); + $this->baseStyle->applyFromArray($array); + } + + public function getStyle(): Style + { + return $this->baseStyle; + } + + public function mergeStyle(Style $style): void + { + if ($style->getNumberFormat()->getFormatCode() !== null) { + $this->baseStyle->getNumberFormat()->setFormatCode($style->getNumberFormat()->getFormatCode()); + } + $this->mergeFontStyle($this->baseStyle->getFont(), $style->getFont()); + $this->mergeFillStyle($this->baseStyle->getFill(), $style->getFill()); + $this->mergeBordersStyle($this->baseStyle->getBorders(), $style->getBorders()); + } + + protected function mergeFontStyle(Font $baseFontStyle, Font $fontStyle): void + { + if ($fontStyle->getBold() !== null) { + $baseFontStyle->setBold($fontStyle->getBold()); + } + if ($fontStyle->getItalic() !== null) { + $baseFontStyle->setItalic($fontStyle->getItalic()); + } + if ($fontStyle->getStrikethrough() !== null) { + $baseFontStyle->setStrikethrough($fontStyle->getStrikethrough()); + } + if ($fontStyle->getUnderline() !== null) { + $baseFontStyle->setUnderline($fontStyle->getUnderline()); + } + if ($fontStyle->getColor()->getARGB() !== null) { + $baseFontStyle->setColor($fontStyle->getColor()); + } + } + + protected function mergeFillStyle(Fill $baseFillStyle, Fill $fillStyle): void + { + if ($fillStyle->getFillType() !== null) { + $baseFillStyle->setFillType($fillStyle->getFillType()); + } + $baseFillStyle->setRotation($fillStyle->getRotation()); + if ($fillStyle->getStartColor()->getARGB() !== null) { + $baseFillStyle->setStartColor($fillStyle->getStartColor()); + } + if ($fillStyle->getEndColor()->getARGB() !== null) { + $baseFillStyle->setEndColor($fillStyle->getEndColor()); + } + } + + protected function mergeBordersStyle(Borders $baseBordersStyle, Borders $bordersStyle): void + { + $this->mergeBorderStyle($baseBordersStyle->getTop(), $bordersStyle->getTop()); + $this->mergeBorderStyle($baseBordersStyle->getBottom(), $bordersStyle->getBottom()); + $this->mergeBorderStyle($baseBordersStyle->getLeft(), $bordersStyle->getLeft()); + $this->mergeBorderStyle($baseBordersStyle->getRight(), $bordersStyle->getRight()); + } + + protected function mergeBorderStyle(Border $baseBorderStyle, Border $borderStyle): void + { + if ($borderStyle->getBorderStyle() !== Border::BORDER_OMIT) { + $baseBorderStyle->setBorderStyle( + $borderStyle->getBorderStyle() + ); + } + if ($borderStyle->getColor()->getARGB() !== null) { + $baseBorderStyle->setColor($borderStyle->getColor()); + } + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php new file mode 100644 index 0000000..2791a2b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php @@ -0,0 +1,66 @@ +cellRange = $cellRange; + } + + public function newRule(string $ruleType): WizardInterface + { + return match ($ruleType) { + self::CELL_VALUE => new Wizard\CellValue($this->cellRange), + self::TEXT_VALUE => new Wizard\TextValue($this->cellRange), + self::BLANKS => new Wizard\Blanks($this->cellRange, true), + self::NOT_BLANKS => new Wizard\Blanks($this->cellRange, false), + self::ERRORS => new Wizard\Errors($this->cellRange, true), + self::NOT_ERRORS => new Wizard\Errors($this->cellRange, false), + self::EXPRESSION, self::FORMULA => new Wizard\Expression($this->cellRange), + self::DATES_OCCURRING => new Wizard\DateValue($this->cellRange), + self::DUPLICATES => new Wizard\Duplicates($this->cellRange, false), + self::UNIQUE => new Wizard\Duplicates($this->cellRange, true), + default => throw new Exception('No wizard exists for this CF rule type'), + }; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + $conditionalType = $conditional->getConditionType(); + + return match ($conditionalType) { + Conditional::CONDITION_CELLIS => Wizard\CellValue::fromConditional($conditional, $cellRange), + Conditional::CONDITION_CONTAINSTEXT, Conditional::CONDITION_NOTCONTAINSTEXT, Conditional::CONDITION_BEGINSWITH, Conditional::CONDITION_ENDSWITH => Wizard\TextValue::fromConditional($conditional, $cellRange), + Conditional::CONDITION_CONTAINSBLANKS, Conditional::CONDITION_NOTCONTAINSBLANKS => Wizard\Blanks::fromConditional($conditional, $cellRange), + Conditional::CONDITION_CONTAINSERRORS, Conditional::CONDITION_NOTCONTAINSERRORS => Wizard\Errors::fromConditional($conditional, $cellRange), + Conditional::CONDITION_TIMEPERIOD => Wizard\DateValue::fromConditional($conditional, $cellRange), + Conditional::CONDITION_EXPRESSION => Wizard\Expression::fromConditional($conditional, $cellRange), + Conditional::CONDITION_DUPLICATES, Conditional::CONDITION_UNIQUE => Wizard\Duplicates::fromConditional($conditional, $cellRange), + default => throw new Exception('No wizard exists for this CF rule type'), + }; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php new file mode 100644 index 0000000..5a8fe9b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php @@ -0,0 +1,95 @@ + false, + 'isBlank' => true, + 'notEmpty' => false, + 'empty' => true, + ]; + + protected const EXPRESSIONS = [ + Wizard::NOT_BLANKS => 'LEN(TRIM(%s))>0', + Wizard::BLANKS => 'LEN(TRIM(%s))=0', + ]; + + protected bool $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + protected function getExpression(): void + { + $this->expression = sprintf( + self::EXPRESSIONS[$this->inverse ? Wizard::BLANKS : Wizard::NOT_BLANKS], + $this->referenceCell + ); + } + + public function getConditional(): Conditional + { + $this->getExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_CONTAINSBLANKS : Conditional::CONDITION_NOTCONTAINSBLANKS + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_CONTAINSBLANKS + && $conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSBLANKS + ) { + throw new Exception('Conditional is not a Blanks CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSBLANKS; + + return $wizard; + } + + /** + * @param mixed[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Blanks CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php new file mode 100644 index 0000000..032bb98 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php @@ -0,0 +1,187 @@ + Conditional::OPERATOR_EQUAL, + 'notEquals' => Conditional::OPERATOR_NOTEQUAL, + 'greaterThan' => Conditional::OPERATOR_GREATERTHAN, + 'greaterThanOrEqual' => Conditional::OPERATOR_GREATERTHANOREQUAL, + 'lessThan' => Conditional::OPERATOR_LESSTHAN, + 'lessThanOrEqual' => Conditional::OPERATOR_LESSTHANOREQUAL, + 'between' => Conditional::OPERATOR_BETWEEN, + 'notBetween' => Conditional::OPERATOR_NOTBETWEEN, + ]; + + protected const SINGLE_OPERATORS = CellMatcher::COMPARISON_OPERATORS; + + protected const RANGE_OPERATORS = CellMatcher::COMPARISON_RANGE_OPERATORS; + + protected string $operator = Conditional::OPERATOR_EQUAL; + + /** @var array */ + protected array $operand = [0]; + + /** + * @var string[] + */ + protected array $operandValueType = []; + + public function __construct(string $cellRange) + { + parent::__construct($cellRange); + } + + protected function operator(string $operator): void + { + if ((!isset(self::SINGLE_OPERATORS[$operator])) && (!isset(self::RANGE_OPERATORS[$operator]))) { + throw new Exception('Invalid Operator for Cell Value CF Rule Wizard'); + } + + $this->operator = $operator; + } + + protected function operand(int $index, mixed $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void + { + if (is_string($operand)) { + $operand = $this->validateOperand($operand, $operandValueType); + } + + $this->operand[$index] = $operand; //* @phpstan-ignore-line + $this->operandValueType[$index] = $operandValueType; + } + + /** @param null|bool|float|int|string $value value to be wrapped */ + protected function wrapValue(mixed $value, string $operandValueType): float|int|string + { + if (!is_numeric($value) && !is_bool($value) && null !== $value) { + if ($operandValueType === Wizard::VALUE_TYPE_LITERAL) { + return '"' . str_replace('"', '""', $value) . '"'; + } + + return $this->cellConditionCheck($value); + } + + if (null === $value) { + $value = 'NULL'; + } elseif (is_bool($value)) { + $value = $value ? 'TRUE' : 'FALSE'; + } + + return $value; + } + + public function getConditional(): Conditional + { + if (!isset(self::RANGE_OPERATORS[$this->operator])) { + unset($this->operand[1], $this->operandValueType[1]); + } + $values = array_map([$this, 'wrapValue'], $this->operand, $this->operandValueType); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_CELLIS); + $conditional->setOperatorType($this->operator); + $conditional->setConditions($values); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + protected static function unwrapString(string $condition): string + { + if ((str_starts_with($condition, '"')) && (str_starts_with(strrev($condition), '"'))) { + $condition = substr($condition, 1, -1); + } + + return str_replace('""', '"', $condition); + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_CELLIS) { + throw new Exception('Conditional is not a Cell Value CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + + $wizard->operator = $conditional->getOperatorType(); + $conditions = $conditional->getConditions(); + foreach ($conditions as $index => $condition) { + // Best-guess to try and identify if the text is a string literal, a cell reference or a formula? + $operandValueType = Wizard::VALUE_TYPE_LITERAL; + if (is_string($condition)) { + if (Calculation::keyInExcelConstants($condition)) { + $condition = Calculation::getExcelConstants($condition); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) { + $operandValueType = Wizard::VALUE_TYPE_CELL; + $condition = self::reverseAdjustCellRef($condition, $cellRange); + } elseif ( + preg_match('/\(\)/', $condition) + || preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition) + ) { + $operandValueType = Wizard::VALUE_TYPE_FORMULA; + $condition = self::reverseAdjustCellRef($condition, $cellRange); + } else { + $condition = self::unwrapString($condition); + } + } + $wizard->operand($index, $condition, $operandValueType); + } + + return $wizard; + } + + /** + * @param mixed[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if (!isset(self::MAGIC_OPERATIONS[$methodName]) && $methodName !== 'and') { + throw new Exception('Invalid Operator for Cell Value CF Rule Wizard'); + } + + if ($methodName === 'and') { + if (!isset(self::RANGE_OPERATORS[$this->operator])) { + throw new Exception('AND Value is only appropriate for range operators'); + } + + $this->operand(1, ...$arguments); + + return $this; + } + + $this->operator(self::MAGIC_OPERATIONS[$methodName]); + //$this->operand(0, ...$arguments); + if (count($arguments) < 2) { + $this->operand(0, $arguments[0]); + } else { + /** @var string */ + $arg1 = $arguments[1]; + $this->operand(0, $arguments[0], $arg1); + } + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php new file mode 100644 index 0000000..23477e9 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php @@ -0,0 +1,109 @@ + Conditional::TIMEPERIOD_YESTERDAY, + 'today' => Conditional::TIMEPERIOD_TODAY, + 'tomorrow' => Conditional::TIMEPERIOD_TOMORROW, + 'lastSevenDays' => Conditional::TIMEPERIOD_LAST_7_DAYS, + 'last7Days' => Conditional::TIMEPERIOD_LAST_7_DAYS, + 'lastWeek' => Conditional::TIMEPERIOD_LAST_WEEK, + 'thisWeek' => Conditional::TIMEPERIOD_THIS_WEEK, + 'nextWeek' => Conditional::TIMEPERIOD_NEXT_WEEK, + 'lastMonth' => Conditional::TIMEPERIOD_LAST_MONTH, + 'thisMonth' => Conditional::TIMEPERIOD_THIS_MONTH, + 'nextMonth' => Conditional::TIMEPERIOD_NEXT_MONTH, + ]; + + protected const EXPRESSIONS = [ + Conditional::TIMEPERIOD_YESTERDAY => 'FLOOR(%s,1)=TODAY()-1', + Conditional::TIMEPERIOD_TODAY => 'FLOOR(%s,1)=TODAY()', + Conditional::TIMEPERIOD_TOMORROW => 'FLOOR(%s,1)=TODAY()+1', + Conditional::TIMEPERIOD_LAST_7_DAYS => 'AND(TODAY()-FLOOR(%s,1)<=6,FLOOR(%s,1)<=TODAY())', + Conditional::TIMEPERIOD_LAST_WEEK => 'AND(TODAY()-ROUNDDOWN(%s,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(%s,0)<(WEEKDAY(TODAY())+7))', + Conditional::TIMEPERIOD_THIS_WEEK => 'AND(TODAY()-ROUNDDOWN(%s,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(%s,0)-TODAY()<=7-WEEKDAY(TODAY()))', + Conditional::TIMEPERIOD_NEXT_WEEK => 'AND(ROUNDDOWN(%s,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(%s,0)-TODAY()<(15-WEEKDAY(TODAY())))', + Conditional::TIMEPERIOD_LAST_MONTH => 'AND(MONTH(%s)=MONTH(EDATE(TODAY(),0-1)),YEAR(%s)=YEAR(EDATE(TODAY(),0-1)))', + Conditional::TIMEPERIOD_THIS_MONTH => 'AND(MONTH(%s)=MONTH(TODAY()),YEAR(%s)=YEAR(TODAY()))', + Conditional::TIMEPERIOD_NEXT_MONTH => 'AND(MONTH(%s)=MONTH(EDATE(TODAY(),0+1)),YEAR(%s)=YEAR(EDATE(TODAY(),0+1)))', + ]; + + protected string $operator; + + public function __construct(string $cellRange) + { + parent::__construct($cellRange); + } + + protected function operator(string $operator): void + { + $this->operator = $operator; + } + + protected function setExpression(): void + { + $referenceCount = substr_count(self::EXPRESSIONS[$this->operator], '%s'); + $references = array_fill(0, $referenceCount, $this->referenceCell); + $this->expression = sprintf(self::EXPRESSIONS[$this->operator], ...$references); + } + + public function getConditional(): Conditional + { + $this->setExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_TIMEPERIOD); + $conditional->setText($this->operator); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_TIMEPERIOD) { + throw new Exception('Conditional is not a Date Value CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->operator = $conditional->getText(); + + return $wizard; + } + + /** + * @param mixed[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if (!isset(self::MAGIC_OPERATIONS[$methodName])) { + throw new Exception('Invalid Operation for Date Value CF Rule Wizard'); + } + + $this->operator(self::MAGIC_OPERATIONS[$methodName]); + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php new file mode 100644 index 0000000..0fbeddb --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php @@ -0,0 +1,74 @@ + false, + 'unique' => true, + ]; + + protected bool $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + public function getConditional(): Conditional + { + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_UNIQUE : Conditional::CONDITION_DUPLICATES + ); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_DUPLICATES + && $conditional->getConditionType() !== Conditional::CONDITION_UNIQUE + ) { + throw new Exception('Conditional is not a Duplicates CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_UNIQUE; + + return $wizard; + } + + /** + * @param mixed[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Errors CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php new file mode 100644 index 0000000..03a2381 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php @@ -0,0 +1,91 @@ + false, + 'isError' => true, + ]; + + protected const EXPRESSIONS = [ + Wizard::NOT_ERRORS => 'NOT(ISERROR(%s))', + Wizard::ERRORS => 'ISERROR(%s)', + ]; + + protected bool $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + protected function getExpression(): void + { + $this->expression = sprintf( + self::EXPRESSIONS[$this->inverse ? Wizard::ERRORS : Wizard::NOT_ERRORS], + $this->referenceCell + ); + } + + public function getConditional(): Conditional + { + $this->getExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_CONTAINSERRORS : Conditional::CONDITION_NOTCONTAINSERRORS + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_CONTAINSERRORS + && $conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSERRORS + ) { + throw new Exception('Conditional is not an Errors CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSERRORS; + + return $wizard; + } + + /** + * @param mixed[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Errors CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php new file mode 100644 index 0000000..7623158 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php @@ -0,0 +1,70 @@ +validateOperand($expression, Wizard::VALUE_TYPE_FORMULA); + $this->expression = $expression; + + return $this; + } + + public function getConditional(): Conditional + { + /** @var string[] */ + $expression = $this->adjustConditionsForCellReferences([$this->expression]); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_EXPRESSION); + $conditional->setConditions($expression); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_EXPRESSION) { + throw new Exception('Conditional is not an Expression CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->expression = self::reverseAdjustCellRef((string) ($conditional->getConditions()[0]), $cellRange); + + return $wizard; + } + + /** + * @param string[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if ($methodName !== 'formula') { + throw new Exception('Invalid Operation for Expression CF Rule Wizard'); + } + + $this->expression(...$arguments); + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php new file mode 100644 index 0000000..b25c4f4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php @@ -0,0 +1,164 @@ + Conditional::OPERATOR_CONTAINSTEXT, + 'doesntContain' => Conditional::OPERATOR_NOTCONTAINS, + 'doesNotContain' => Conditional::OPERATOR_NOTCONTAINS, + 'beginsWith' => Conditional::OPERATOR_BEGINSWITH, + 'startsWith' => Conditional::OPERATOR_BEGINSWITH, + 'endsWith' => Conditional::OPERATOR_ENDSWITH, + ]; + + protected const OPERATORS = [ + Conditional::OPERATOR_CONTAINSTEXT => Conditional::CONDITION_CONTAINSTEXT, + Conditional::OPERATOR_NOTCONTAINS => Conditional::CONDITION_NOTCONTAINSTEXT, + Conditional::OPERATOR_BEGINSWITH => Conditional::CONDITION_BEGINSWITH, + Conditional::OPERATOR_ENDSWITH => Conditional::CONDITION_ENDSWITH, + ]; + + protected const EXPRESSIONS = [ + Conditional::OPERATOR_CONTAINSTEXT => 'NOT(ISERROR(SEARCH(%s,%s)))', + Conditional::OPERATOR_NOTCONTAINS => 'ISERROR(SEARCH(%s,%s))', + Conditional::OPERATOR_BEGINSWITH => 'LEFT(%s,LEN(%s))=%s', + Conditional::OPERATOR_ENDSWITH => 'RIGHT(%s,LEN(%s))=%s', + ]; + + protected string $operator; + + protected string $operand; + + protected string $operandValueType; + + public function __construct(string $cellRange) + { + parent::__construct($cellRange); + } + + protected function operator(string $operator): void + { + if (!isset(self::OPERATORS[$operator])) { + throw new Exception('Invalid Operator for Text Value CF Rule Wizard'); + } + + $this->operator = $operator; + } + + protected function operand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void + { + $operand = $this->validateOperand($operand, $operandValueType); + + $this->operand = $operand; + $this->operandValueType = $operandValueType; + } + + protected function wrapValue(string $value): string + { + return '"' . $value . '"'; + } + + protected function setExpression(): void + { + $operand = $this->operandValueType === Wizard::VALUE_TYPE_LITERAL + ? $this->wrapValue(str_replace('"', '""', $this->operand)) + : $this->cellConditionCheck($this->operand); + + if ( + $this->operator === Conditional::OPERATOR_CONTAINSTEXT + || $this->operator === Conditional::OPERATOR_NOTCONTAINS + ) { + $this->expression = sprintf(self::EXPRESSIONS[$this->operator], $operand, $this->referenceCell); + } else { + $this->expression = sprintf(self::EXPRESSIONS[$this->operator], $this->referenceCell, $operand, $operand); + } + } + + public function getConditional(): Conditional + { + $this->setExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType(self::OPERATORS[$this->operator]); + $conditional->setOperatorType($this->operator); + $conditional->setText( + $this->operandValueType !== Wizard::VALUE_TYPE_LITERAL + ? $this->cellConditionCheck($this->operand) + : $this->operand + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if (!in_array($conditional->getConditionType(), self::OPERATORS, true)) { + throw new Exception('Conditional is not a Text Value CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->operator = (string) array_search($conditional->getConditionType(), self::OPERATORS, true); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + + // Best-guess to try and identify if the text is a string literal, a cell reference or a formula? + $wizard->operandValueType = Wizard::VALUE_TYPE_LITERAL; + $condition = $conditional->getText(); + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) { + $wizard->operandValueType = Wizard::VALUE_TYPE_CELL; + $condition = self::reverseAdjustCellRef($condition, $cellRange); + } elseif ( + preg_match('/\(\)/', $condition) + || preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition) + ) { + $wizard->operandValueType = Wizard::VALUE_TYPE_FORMULA; + } + $wizard->operand = $condition; + + return $wizard; + } + + /** + * @param mixed[] $arguments + */ + public function __call(string $methodName, array $arguments): self + { + if (!isset(self::MAGIC_OPERATIONS[$methodName])) { + throw new Exception('Invalid Operation for Text Value CF Rule Wizard'); + } + + $this->operator(self::MAGIC_OPERATIONS[$methodName]); + //$this->operand(...$arguments); + if (count($arguments) < 2) { + /** @var string */ + $arg0 = $arguments[0]; + $this->operand($arg0); + } else { + /** @var string */ + $arg0 = $arguments[0]; + /** @var string */ + $arg1 = $arguments[1]; + $this->operand($arg0, $arg1); + } + + return $this; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php new file mode 100644 index 0000000..9f64ad1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php @@ -0,0 +1,183 @@ +setCellRange($cellRange); + } + + public function getCellRange(): string + { + return $this->cellRange; + } + + public function setCellRange(string $cellRange): void + { + $this->cellRange = $cellRange; + $this->setReferenceCellForExpressions($cellRange); + } + + protected function setReferenceCellForExpressions(string $conditionalRange): void + { + $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange))); + [$this->referenceCell] = $conditionalRange[0]; + + [$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell); + } + + public function getStopIfTrue(): bool + { + return $this->stopIfTrue; + } + + public function setStopIfTrue(bool $stopIfTrue): void + { + $this->stopIfTrue = $stopIfTrue; + } + + public function getStyle(): Style + { + return $this->style ?? new Style(false, true); + } + + public function setStyle(Style $style): void + { + $this->style = $style; + } + + protected function validateOperand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): string + { + if ( + $operandValueType === Wizard::VALUE_TYPE_LITERAL + && str_starts_with($operand, '"') + && str_ends_with($operand, '"') + ) { + $operand = str_replace('""', '"', substr($operand, 1, -1)); + } elseif ($operandValueType === Wizard::VALUE_TYPE_FORMULA && str_starts_with($operand, '=')) { + $operand = substr($operand, 1); + } + + return $operand; + } + + /** @param string[] $matches */ + protected static function reverseCellAdjustment(array $matches, int $referenceColumn, int $referenceRow): string + { + $worksheet = $matches[1]; + $column = $matches[6]; + $row = $matches[7]; + + if (!str_contains($column, '$')) { + $column = Coordinate::columnIndexFromString($column); + $column -= $referenceColumn - 1; + $column = Coordinate::stringFromColumnIndex($column); + } + + if (!str_contains($row, '$')) { + $row = (int) $row - ($referenceRow - 1); + } + + return "{$worksheet}{$column}{$row}"; + } + + public static function reverseAdjustCellRef(string $condition, string $cellRange): string + { + $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellRange))); + [$referenceCell] = $conditionalRange[0]; + [$referenceColumnIndex, $referenceRow] = Coordinate::indexesFromString($referenceCell); + + $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition); + $i = false; + foreach ($splitCondition as &$value) { + // Only count/replace in alternating array entries (ie. not in quoted strings) + $i = $i === false; + if ($i) { + $value = (string) preg_replace_callback( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', + fn ($matches): string => self::reverseCellAdjustment($matches, $referenceColumnIndex, $referenceRow), + $value + ); + } + } + unset($value); + + // Then rebuild the condition string to return it + return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition); + } + + /** @param string[] $matches */ + protected function conditionCellAdjustment(array $matches): string + { + $worksheet = $matches[1]; + $column = $matches[6]; + $row = $matches[7]; + + if (!str_contains($column, '$')) { + $column = Coordinate::columnIndexFromString($column); + $column += $this->referenceColumn - 1; + $column = Coordinate::stringFromColumnIndex($column); + } + + if (!str_contains($row, '$')) { + $row = (int) $row + ($this->referenceRow - 1); + } + + return "{$worksheet}{$column}{$row}"; + } + + protected function cellConditionCheck(string $condition): string + { + $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition); + $i = false; + foreach ($splitCondition as &$value) { + // Only count/replace in alternating array entries (ie. not in quoted strings) + $i = $i === false; + if ($i) { + $value = (string) preg_replace_callback( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', + [$this, 'conditionCellAdjustment'], + $value + ); + } + } + unset($value); + + // Then rebuild the condition string to return it + return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition); + } + + /** + * @param mixed[] $conditions + * + * @return mixed[] + */ + protected function adjustConditionsForCellReferences(array $conditions): array + { + return array_map( + [$this, 'cellConditionCheck'], + $conditions + ); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php new file mode 100644 index 0000000..10ad57b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php @@ -0,0 +1,25 @@ +fillType = null; + } + $this->startColor = new Color(Color::COLOR_WHITE, $isSupervisor, $isConditional); + $this->endColor = new Color(Color::COLOR_BLACK, $isSupervisor, $isConditional); + + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->startColor->bindParent($this, 'startColor'); + $this->endColor->bindParent($this, 'endColor'); + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getFill(); + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return array{fill: mixed[]} + */ + public function getStyleArray(array $array): array + { + return ['fill' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFill()->applyFromArray( + * [ + * 'fillType' => Fill::FILL_GRADIENT_LINEAR, + * 'rotation' => 0.0, + * 'startColor' => [ + * 'rgb' => '000000' + * ], + * 'endColor' => [ + * 'argb' => 'FFFFFFFF' + * ] + * ] + * ); + * + * + * @param array{fillType?: string, rotation?: float, startColor?: array{rgb?: string, argb?: string}, endColor?: array{rgb?: string, argb?: string}, color?: array{rgb?: string, argb?: string}} $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + if (isset($styleArray['fillType'])) { + $this->setFillType($styleArray['fillType']); + } + if (isset($styleArray['rotation'])) { + $this->setRotation($styleArray['rotation']); + } + if (isset($styleArray['startColor'])) { + $this->getStartColor() + ->applyFromArray($styleArray['startColor']); + } + if (isset($styleArray['endColor'])) { + $this->getEndColor() + ->applyFromArray($styleArray['endColor']); + } + if (isset($styleArray['color'])) { + $this->getStartColor() + ->applyFromArray($styleArray['color']); + $this->getEndColor() + ->applyFromArray($styleArray['color']); + } + } + + return $this; + } + + /** + * Get Fill Type. + */ + public function getFillType(): ?string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getFillType(); + } + + return $this->fillType; + } + + /** + * Set Fill Type. + * + * @param string $fillType Fill type, see self::FILL_* + * + * @return $this + */ + public function setFillType(string $fillType): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['fillType' => $fillType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->fillType = $fillType; + } + + return $this; + } + + /** + * Get Rotation. + */ + public function getRotation(): float + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getRotation(); + } + + return $this->rotation; + } + + /** + * Set Rotation. + * + * @return $this + */ + public function setRotation(float $angleInDegrees): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['rotation' => $angleInDegrees]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->rotation = $angleInDegrees; + } + + return $this; + } + + /** + * Get Start Color. + */ + public function getStartColor(): Color + { + return $this->startColor; + } + + /** + * Set Start Color. + * + * @return $this + */ + public function setStartColor(Color $color): static + { + $this->colorChanged = true; + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getStartColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->startColor = $color; + } + + return $this; + } + + /** + * Get End Color. + */ + public function getEndColor(): Color + { + return $this->endColor; + } + + /** + * Set End Color. + * + * @return $this + */ + public function setEndColor(Color $color): static + { + $this->colorChanged = true; + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getEndColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->endColor = $color; + } + + return $this; + } + + public function getColorsChanged(): bool + { + if ($this->isSupervisor) { + $changed = $this->getSharedComponent()->colorChanged; + } else { + $changed = $this->colorChanged; + } + + return $changed || $this->startColor->getHasChanged() || $this->endColor->getHasChanged(); + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + // Note that we don't care about colours for fill type NONE, but could have duplicate NONEs with + // different hashes if we don't explicitly prevent this + return md5( + $this->getFillType() + . $this->getRotation() + . ($this->getFillType() !== self::FILL_NONE ? $this->getStartColor()->getHashCode() : '') + . ($this->getFillType() !== self::FILL_NONE ? $this->getEndColor()->getHashCode() : '') + . ((string) $this->getColorsChanged()) + . __CLASS__ + ); + } + + /** @return mixed[] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'fillType', $this->getFillType()); + $this->exportArray2($exportedArray, 'rotation', $this->getRotation()); + if ($this->getColorsChanged()) { + $this->exportArray2($exportedArray, 'endColor', $this->getEndColor()); + $this->exportArray2($exportedArray, 'startColor', $this->getStartColor()); + } + + return $exportedArray; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Font.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Font.php new file mode 100644 index 0000000..ae5ec6c --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/Font.php @@ -0,0 +1,886 @@ +name = null; + $this->size = null; + $this->bold = null; + $this->italic = null; + $this->superscript = null; + $this->subscript = null; + $this->underline = null; + $this->strikethrough = null; + $this->color = new Color(Color::COLOR_BLACK, $isSupervisor, $isConditional); + } else { + $this->color = new Color(Color::COLOR_BLACK, $isSupervisor); + } + // bind parent if we are a supervisor + if ($isSupervisor) { + $this->color->bindParent($this, 'color'); + } + } + + public function applyThemeFonts(Theme $theme): void + { + $this->setName($theme->getMinorFontLatin()); + $this->setLatin($theme->getMinorFontLatin()); + $this->setEastAsian($theme->getMinorFontEastAsian()); + $this->setComplexScript($theme->getMinorFontComplexScript()); + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getFont(); + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return array{font: mixed[]} + */ + public function getStyleArray(array $array): array + { + return ['font' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->applyFromArray( + * [ + * 'name' => 'Arial', + * 'bold' => TRUE, + * 'italic' => FALSE, + * 'underline' => \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE, + * 'strikethrough' => FALSE, + * 'color' => [ + * 'rgb' => '808080' + * ] + * ] + * ); + * + * + * @param array{name?: string, latin?: string, eastAsian?: string, complexScript?: string, bold?: bool, italic?: bool, superscript?: bool, subscript?: bool, underline?: bool|string, strikethrough?: bool, color?: string[], size?: ?int, chartColor?: ChartColor, scheme?: string, cap?: string, autoColor?: bool} $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + if (isset($styleArray['name'])) { + $this->setName($styleArray['name']); + } + if (isset($styleArray['latin'])) { + $this->setLatin($styleArray['latin']); + } + if (isset($styleArray['eastAsian'])) { + $this->setEastAsian($styleArray['eastAsian']); + } + if (isset($styleArray['complexScript'])) { + $this->setComplexScript( + $styleArray['complexScript'] + ); + } + if (isset($styleArray['bold'])) { + $this->setBold($styleArray['bold']); + } + if (isset($styleArray['italic'])) { + $this->setItalic($styleArray['italic']); + } + if (isset($styleArray['superscript'])) { + $this->setSuperscript($styleArray['superscript']); + } + if (isset($styleArray['subscript'])) { + $this->setSubscript($styleArray['subscript']); + } + if (isset($styleArray['underline'])) { + $this->setUnderline($styleArray['underline']); + } + if (isset($styleArray['strikethrough'])) { + $this->setStrikethrough( + $styleArray['strikethrough'] + ); + } + if (isset($styleArray['color'])) { + /** @var array{rgb?: string, argb?: string, theme?: int} */ + $temp = $styleArray['color']; + $this->getColor() + ->applyFromArray($temp); + } + if (isset($styleArray['size'])) { + $this->setSize($styleArray['size']); + } + if (isset($styleArray['chartColor'])) { + $this->chartColor = $styleArray['chartColor']; + } + if (isset($styleArray['scheme'])) { + $this->setScheme($styleArray['scheme']); + } + if (isset($styleArray['cap'])) { + $this->setCap($styleArray['cap']); + } + if (isset($styleArray['autoColor'])) { + $this->setAutoColor($styleArray['autoColor']); + } + } + + return $this; + } + + /** + * Get Name. + */ + public function getName(): ?string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getName(); + } + + return $this->name; + } + + public function getLatin(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getLatin(); + } + + return $this->latin; + } + + public function getEastAsian(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getEastAsian(); + } + + return $this->eastAsian; + } + + public function getComplexScript(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getComplexScript(); + } + + return $this->complexScript; + } + + /** + * Set Name and turn off Scheme. + */ + public function setName(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['name' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->name = $fontname; + } + + return $this->setScheme(''); + } + + public function setLatin(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if (!$this->isSupervisor) { + $this->latin = $fontname; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['latin' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function setEastAsian(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if (!$this->isSupervisor) { + $this->eastAsian = $fontname; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['eastAsian' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function setComplexScript(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if (!$this->isSupervisor) { + $this->complexScript = $fontname; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['complexScript' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + /** + * Get Size. + */ + public function getSize(): ?float + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSize(); + } + + return $this->size; + } + + /** + * Set Size. + * + * @param mixed $sizeInPoints A float representing the value of a positive measurement in points (1/72 of an inch) + * + * @return $this + */ + public function setSize(mixed $sizeInPoints, bool $nullOk = false): static + { + if (is_string($sizeInPoints) || is_int($sizeInPoints)) { + $sizeInPoints = (float) $sizeInPoints; // $pValue = 0 if given string is not numeric + } + + // Size must be a positive floating point number + // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 + if (!is_float($sizeInPoints) || !($sizeInPoints > 0)) { + if (!$nullOk || $sizeInPoints !== null) { + $sizeInPoints = 10.0; + } + } + + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['size' => $sizeInPoints]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->size = $sizeInPoints; + } + + return $this; + } + + /** + * Get Bold. + */ + public function getBold(): ?bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBold(); + } + + return $this->bold; + } + + /** + * Set Bold. + * + * @return $this + */ + public function setBold(bool $bold): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['bold' => $bold]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->bold = $bold; + } + + return $this; + } + + /** + * Get Italic. + */ + public function getItalic(): ?bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getItalic(); + } + + return $this->italic; + } + + /** + * Set Italic. + * + * @return $this + */ + public function setItalic(bool $italic): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['italic' => $italic]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->italic = $italic; + } + + return $this; + } + + /** + * Get Superscript. + */ + public function getSuperscript(): ?bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSuperscript(); + } + + return $this->superscript; + } + + /** + * Set Superscript. + * + * @return $this + */ + public function setSuperscript(bool $superscript): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['superscript' => $superscript]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->superscript = $superscript; + if ($this->superscript) { + $this->subscript = false; + } + } + + return $this; + } + + /** + * Get Subscript. + */ + public function getSubscript(): ?bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getSubscript(); + } + + return $this->subscript; + } + + /** + * Set Subscript. + * + * @return $this + */ + public function setSubscript(bool $subscript): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['subscript' => $subscript]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->subscript = $subscript; + if ($this->subscript) { + $this->superscript = false; + } + } + + return $this; + } + + public function getBaseLine(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBaseLine(); + } + + return $this->baseLine; + } + + public function setBaseLine(int $baseLine): self + { + if (!$this->isSupervisor) { + $this->baseLine = $baseLine; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['baseLine' => $baseLine]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function getStrikeType(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikeType(); + } + + return $this->strikeType; + } + + public function setStrikeType(string $strikeType): self + { + if (!$this->isSupervisor) { + $this->strikeType = $strikeType; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['strikeType' => $strikeType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function getUnderlineColor(): ?ChartColor + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUnderlineColor(); + } + + return $this->underlineColor; + } + + /** @param array{value: null|string, alpha: null|int|string, brightness?: null|int|string, type: null|string} $colorArray */ + public function setUnderlineColor(array $colorArray): self + { + if (!$this->isSupervisor) { + $this->underlineColor = new ChartColor($colorArray); + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['underlineColor' => $colorArray]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function getChartColor(): ?ChartColor + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getChartColor(); + } + + return $this->chartColor; + } + + /** @param array{value: null|string, alpha: null|int|string, brightness?: null|int|string, type: null|string} $colorArray */ + public function setChartColor(array $colorArray): self + { + if (!$this->isSupervisor) { + $this->chartColor = new ChartColor($colorArray); + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['chartColor' => $colorArray]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function setChartColorFromObject(?ChartColor $chartColor): self + { + $this->chartColor = $chartColor; + + return $this; + } + + /** + * Get Underline. + */ + public function getUnderline(): ?string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUnderline(); + } + + return $this->underline; + } + + /** + * Set Underline. + * + * @param bool|string $underlineStyle \PhpOffice\PhpSpreadsheet\Style\Font underline type + * If a boolean is passed, then TRUE equates to UNDERLINE_SINGLE, + * false equates to UNDERLINE_NONE + * + * @return $this + */ + public function setUnderline($underlineStyle): static + { + if (is_bool($underlineStyle)) { + $underlineStyle = ($underlineStyle) ? self::UNDERLINE_SINGLE : self::UNDERLINE_NONE; + } elseif ($underlineStyle == '') { + $underlineStyle = self::UNDERLINE_NONE; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['underline' => $underlineStyle]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->underline = $underlineStyle; + } + + return $this; + } + + /** + * Get Strikethrough. + */ + public function getStrikethrough(): ?bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikethrough(); + } + + return $this->strikethrough; + } + + /** + * Set Strikethrough. + * + * @return $this + */ + public function setStrikethrough(bool $strikethru): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['strikethrough' => $strikethru]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->strikethrough = $strikethru; + } + + return $this; + } + + /** + * Get Color. + */ + public function getColor(): Color + { + return $this->color; + } + + /** + * Set Color. + * + * @return $this + */ + public function setColor(Color $color): static + { + // make sure parameter is a real color and not a supervisor + $color = $color->getIsSupervisor() ? $color->getSharedComponent() : $color; + + if ($this->isSupervisor) { + $styleArray = $this->getColor()->getStyleArray(['argb' => $color->getARGB()]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->color = $color; + } + + return $this; + } + + private function hashChartColor(?ChartColor $underlineColor): string + { + if ($underlineColor === null) { + return ''; + } + + return + $underlineColor->getValue() + . $underlineColor->getType() + . (string) $underlineColor->getAlpha(); + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->name + . $this->size + . ($this->bold ? 't' : 'f') + . ($this->italic ? 't' : 'f') + . ($this->superscript ? 't' : 'f') + . ($this->subscript ? 't' : 'f') + . $this->underline + . ($this->strikethrough ? 't' : 'f') + . ($this->autoColor ? 't' : 'f') + . $this->color->getHashCode() + . $this->scheme + . implode( + '*', + [ + $this->latin, + $this->eastAsian, + $this->complexScript, + $this->strikeType, + $this->hashChartColor($this->chartColor), + $this->hashChartColor($this->underlineColor), + (string) $this->baseLine, + (string) $this->cap, + ] + ) + . __CLASS__ + ); + } + + /** @return mixed[] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); + $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'cap', $this->getCap()); + $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); + $this->exportArray2($exportedArray, 'color', $this->getColor()); + $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); + $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); + $this->exportArray2($exportedArray, 'italic', $this->getItalic()); + $this->exportArray2($exportedArray, 'latin', $this->getLatin()); + $this->exportArray2($exportedArray, 'name', $this->getName()); + $this->exportArray2($exportedArray, 'scheme', $this->getScheme()); + $this->exportArray2($exportedArray, 'size', $this->getSize()); + $this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough()); + $this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType()); + $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); + $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); + $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); + $this->exportArray2($exportedArray, 'underlineColor', $this->getUnderlineColor()); + $this->exportArray2($exportedArray, 'autoColor', $this->getAutoColor()); + + return $exportedArray; + } + + public function getScheme(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getScheme(); + } + + return $this->scheme; + } + + public function setScheme(string $scheme): self + { + if ($scheme === '' || $scheme === 'major' || $scheme === 'minor') { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['scheme' => $scheme]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->scheme = $scheme; + } + } + + return $this; + } + + /** + * Set capitalization attribute. If not one of the permitted + * values (all, small, or none), set it to null. + * This will be honored only for the font for chart titles. + * None is distinguished from null because null will inherit + * the current value, whereas 'none' will override it. + */ + public function setCap(string $cap): self + { + $this->cap = in_array($cap, self::VALID_CAPS, true) ? $cap : null; + + return $this; + } + + public function getCap(): ?string + { + return $this->cap; + } + + public function setHyperlinkTheme(): self + { + $this->color->setHyperlinkTheme(); + $this->setUnderline(self::UNDERLINE_SINGLE); + + return $this; + } + + public function setAutoColor(bool $autoColor): self + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['autoColor' => $autoColor]); + $this->getActiveSheet() + ->getStyle($this->getSelectedCells()) + ->applyFromArray($styleArray); + } else { + $this->autoColor = $autoColor; + } + + return $this; + } + + public function getAutoColor(): bool + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getAutoColor(); + } + + return $this->autoColor; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $this->color = clone $this->color; + $this->chartColor = ($this->chartColor === null) ? null : clone $this->chartColor; + $this->underlineColor = ($this->underlineColor === null) ? null : clone $this->underlineColor; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat.php new file mode 100644 index 0000000..07d0017 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat.php @@ -0,0 +1,527 @@ +formatCode = null; + $this->builtInFormatCode = false; + } + } + + /** + * Get the shared style component for the currently active cell in currently active sheet. + * Only used for style supervisor. + */ + public function getSharedComponent(): self + { + /** @var Style $parent */ + $parent = $this->parent; + + return $parent->getSharedComponent()->getNumberFormat(); + } + + /** + * Build style array from subcomponents. + * + * @param mixed[] $array + * + * @return array{numberFormat: mixed[]} + */ + public function getStyleArray(array $array): array + { + return ['numberFormat' => $array]; + } + + /** + * Apply styles from array. + * + * + * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray( + * [ + * 'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE + * ] + * ); + * + * + * @param string[] $styleArray Array containing style information + * + * @return $this + */ + public function applyFromArray(array $styleArray): static + { + if ($this->isSupervisor) { + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray)); + } else { + if (isset($styleArray['formatCode'])) { + $this->setFormatCode($styleArray['formatCode']); + } + } + + return $this; + } + + /** + * Get Format Code. + */ + public function getFormatCode(bool $extended = false): ?string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getFormatCode($extended); + } + $builtin = $this->getBuiltInFormatCode(); + if (is_int($builtin)) { + if ($extended) { + if ($builtin === self::SHORT_DATE_INDEX) { + return self::$shortDateFormat; + } + if ($builtin === self::DATE_TIME_INDEX) { + return self::$dateTimeFormat; + } + } + + return self::builtInFormatCode($builtin); + } + + return $extended ? self::convertSystemFormats($this->formatCode) : $this->formatCode; + } + + public static function convertSystemFormats(?string $formatCode): ?string + { + if (is_string($formatCode)) { + if (stripos($formatCode, self::FORMAT_SYSDATE_F800) !== false || stripos($formatCode, self::FORMAT_SYSDATE_X) !== false) { + return self::$longDateFormat; + } + if (stripos($formatCode, self::FORMAT_SYSTIME_F400) !== false || stripos($formatCode, self::FORMAT_SYSTIME_X) !== false) { + return self::$timeFormat; + } + } + + return $formatCode; + } + + /** + * Set Format Code. + * + * @param string $formatCode see self::FORMAT_* + * + * @return $this + */ + public function setFormatCode(string $formatCode): static + { + if ($formatCode == '') { + $formatCode = self::FORMAT_GENERAL; + } + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['formatCode' => $formatCode]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->formatCode = $formatCode; + $this->builtInFormatCode = self::builtInFormatCodeIndex($formatCode); + } + + return $this; + } + + /** + * Get Built-In Format Code. + * + * @return false|int + */ + public function getBuiltInFormatCode() + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBuiltInFormatCode(); + } + + return $this->builtInFormatCode; + } + + /** + * Set Built-In Format Code. + * + * @param int $formatCodeIndex Id of the built-in format code to use + * + * @return $this + */ + public function setBuiltInFormatCode(int $formatCodeIndex): static + { + if ($this->isSupervisor) { + $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($formatCodeIndex)]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + } else { + $this->builtInFormatCode = $formatCodeIndex; + $this->formatCode = self::builtInFormatCode($formatCodeIndex); + } + + return $this; + } + + /** + * Fill built-in format codes. + */ + private static function fillBuiltInFormatCodes(): void + { + // [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance] + // 18.8.30. numFmt (Number Format) + // + // The ECMA standard defines built-in format IDs + // 14: "mm-dd-yy" + // 22: "m/d/yy h:mm" + // 37: "#,##0 ;(#,##0)" + // 38: "#,##0 ;[Red](#,##0)" + // 39: "#,##0.00;(#,##0.00)" + // 40: "#,##0.00;[Red](#,##0.00)" + // 47: "mmss.0" + // KOR fmt 55: "yyyy-mm-dd" + // Excel defines built-in format IDs + // 14: "m/d/yyyy" + // 22: "m/d/yyyy h:mm" + // 37: "#,##0_);(#,##0)" + // 38: "#,##0_);[Red](#,##0)" + // 39: "#,##0.00_);(#,##0.00)" + // 40: "#,##0.00_);[Red](#,##0.00)" + // 47: "mm:ss.0" + // KOR fmt 55: "yyyy/mm/dd" + + // Built-in format codes + if (empty(self::$builtInFormats)) { + self::$builtInFormats = []; + + // General + self::$builtInFormats[0] = self::FORMAT_GENERAL; + self::$builtInFormats[1] = '0'; + self::$builtInFormats[2] = '0.00'; + self::$builtInFormats[3] = '#,##0'; + self::$builtInFormats[4] = '#,##0.00'; + + self::$builtInFormats[9] = '0%'; + self::$builtInFormats[10] = '0.00%'; + self::$builtInFormats[11] = '0.00E+00'; + self::$builtInFormats[12] = '# ?/?'; + self::$builtInFormats[13] = '# ??/??'; + self::$builtInFormats[14] = self::FORMAT_DATE_XLSX14_ACTUAL; // Despite ECMA 'mm-dd-yy'; + self::$builtInFormats[15] = self::FORMAT_DATE_XLSX15; + self::$builtInFormats[16] = 'd-mmm'; + self::$builtInFormats[17] = 'mmm-yy'; + self::$builtInFormats[18] = 'h:mm AM/PM'; + self::$builtInFormats[19] = 'h:mm:ss AM/PM'; + self::$builtInFormats[20] = 'h:mm'; + self::$builtInFormats[21] = 'h:mm:ss'; + self::$builtInFormats[22] = self::FORMAT_DATE_XLSX22_ACTUAL; // Despite ECMA 'm/d/yy h:mm'; + + self::$builtInFormats[37] = '#,##0_);(#,##0)'; // Despite ECMA '#,##0 ;(#,##0)'; + self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; // Despite ECMA '#,##0 ;[Red](#,##0)'; + self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; // Despite ECMA '#,##0.00;(#,##0.00)'; + self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; // Despite ECMA '#,##0.00;[Red](#,##0.00)'; + + self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)'; + self::$builtInFormats[45] = 'mm:ss'; + self::$builtInFormats[46] = '[h]:mm:ss'; + self::$builtInFormats[47] = 'mm:ss.0'; // Despite ECMA 'mmss.0'; + self::$builtInFormats[48] = '##0.0E+0'; + self::$builtInFormats[49] = '@'; + + // CHT + self::$builtInFormats[27] = '[$-404]e/m/d'; + self::$builtInFormats[30] = 'm/d/yy'; + self::$builtInFormats[36] = '[$-404]e/m/d'; + self::$builtInFormats[50] = '[$-404]e/m/d'; + self::$builtInFormats[57] = '[$-404]e/m/d'; + + // THA + self::$builtInFormats[59] = 't0'; + self::$builtInFormats[60] = 't0.00'; + self::$builtInFormats[61] = 't#,##0'; + self::$builtInFormats[62] = 't#,##0.00'; + self::$builtInFormats[67] = 't0%'; + self::$builtInFormats[68] = 't0.00%'; + self::$builtInFormats[69] = 't# ?/?'; + self::$builtInFormats[70] = 't# ??/??'; + + // JPN + self::$builtInFormats[28] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[29] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[31] = 'yyyy"年"m"月"d"日"'; + self::$builtInFormats[32] = 'h"時"mm"分"'; + self::$builtInFormats[33] = 'h"時"mm"分"ss"秒"'; + self::$builtInFormats[34] = 'yyyy"年"m"月"'; + self::$builtInFormats[35] = 'm"月"d"日"'; + self::$builtInFormats[51] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[52] = 'yyyy"年"m"月"'; + self::$builtInFormats[53] = 'm"月"d"日"'; + self::$builtInFormats[54] = '[$-411]ggge"年"m"月"d"日"'; + self::$builtInFormats[55] = 'yyyy"年"m"月"'; + self::$builtInFormats[56] = 'm"月"d"日"'; + self::$builtInFormats[58] = '[$-411]ggge"年"m"月"d"日"'; + + // Flip array (for faster lookups) + self::$flippedBuiltInFormats = array_flip(self::$builtInFormats); + } + } + + /** + * Get built-in format code. + */ + public static function builtInFormatCode(int $index): string + { + // Clean parameter + $index = (int) $index; + + // Ensure built-in format codes are available + self::fillBuiltInFormatCodes(); + + // Lookup format code + if (isset(self::$builtInFormats[$index])) { + return self::$builtInFormats[$index]; + } + + return ''; + } + + /** + * Get built-in format code index. + * + * @return false|int + */ + public static function builtInFormatCodeIndex(string $formatCodeIndex) + { + // Ensure built-in format codes are available + self::fillBuiltInFormatCodes(); + + // Lookup format code + if (array_key_exists($formatCodeIndex, self::$flippedBuiltInFormats)) { + return self::$flippedBuiltInFormats[$formatCodeIndex]; + } + + return false; + } + + /** + * Get hash code. + * + * @return string Hash code + */ + public function getHashCode(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getHashCode(); + } + + return md5( + $this->formatCode + . $this->builtInFormatCode + . __CLASS__ + ); + } + + /** + * Convert a value in a pre-defined format to a PHP string. + * + * @param null|bool|float|int|RichText|string $value Value to format + * @param string $format Format code: see = self::FORMAT_* for predefined values; + * or can be any valid MS Excel custom format string + * @param ?mixed[] $callBack Callback function for additional formatting of string + * @param bool $lessFloatPrecision If true, unstyled floats will be converted to a more human-friendly but less computationally accurate value + * + * @return string Formatted string + */ + public static function toFormattedString(mixed $value, string $format, ?array $callBack = null, bool $lessFloatPrecision = false): string + { + return NumberFormat\Formatter::toFormattedString($value, $format, $callBack, $lessFloatPrecision); + } + + /** @return mixed[] */ + protected function exportArray1(): array + { + $exportedArray = []; + $this->exportArray2($exportedArray, 'formatCode', $this->getFormatCode()); + + return $exportedArray; + } + + public static function getShortDateFormat(): string + { + return self::$shortDateFormat; + } + + public static function setShortDateFormat(string $shortDateFormat): void + { + self::$shortDateFormat = $shortDateFormat; + } + + public static function getLongDateFormat(): string + { + return self::$longDateFormat; + } + + public static function setLongDateFormat(string $longDateFormat): void + { + self::$longDateFormat = $longDateFormat; + } + + public static function getDateTimeFormat(): string + { + return self::$dateTimeFormat; + } + + public static function setDateTimeFormat(string $dateTimeFormat): void + { + self::$dateTimeFormat = $dateTimeFormat; + } + + public static function getTimeFormat(): string + { + return self::$timeFormat; + } + + public static function setTimeFormat(string $timeFormat): void + { + self::$timeFormat = $timeFormat; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php new file mode 100644 index 0000000..165779b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php @@ -0,0 +1,25 @@ + '', + // 12-hour suffix + 'am/pm' => 'A', + // 4-digit year + 'e' => 'Y', + 'yyyy' => 'Y', + // 2-digit year + 'yy' => 'y', + // first letter of month - no php equivalent + 'mmmmm' => 'M', + // full month name + 'mmmm' => 'F', + // short month name + 'mmm' => 'M', + // mm is minutes if time, but can also be month w/leading zero + // so we try to identify times be the inclusion of a : separator in the mask + // It isn't perfect, but the best way I know how + ':mm' => ':i', + 'mm:' => 'i:', + // full day of week name + 'dddd' => 'l', + // short day of week name + 'ddd' => 'D', + // days leading zero + 'dd' => 'd', + // days no leading zero + 'd' => 'j', + // fractional seconds - no php equivalent + '.s' => '', + ]; + + /** + * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock). + */ + private const DATE_FORMAT_REPLACEMENTS24 = [ + 'hh' => 'H', + 'h' => 'G', + // month leading zero + 'mm' => 'm', + // month no leading zero + 'm' => 'n', + // seconds + 'ss' => 's', + ]; + + /** + * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock). + */ + private const DATE_FORMAT_REPLACEMENTS12 = [ + 'hh' => 'h', + 'h' => 'g', + // month leading zero + 'mm' => 'm', + // month no leading zero + 'm' => 'n', + // seconds + 'ss' => 's', + ]; + + private const HOURS_IN_DAY = 24; + private const MINUTES_IN_DAY = 60 * self::HOURS_IN_DAY; + private const SECONDS_IN_DAY = 60 * self::MINUTES_IN_DAY; + private const INTERVAL_PRECISION = 10; + private const INTERVAL_LEADING_ZERO = [ + '[hh]', + '[mm]', + '[ss]', + ]; + private const INTERVAL_ROUND_PRECISION = [ + // hours and minutes truncate + '[h]' => self::INTERVAL_PRECISION, + '[hh]' => self::INTERVAL_PRECISION, + '[m]' => self::INTERVAL_PRECISION, + '[mm]' => self::INTERVAL_PRECISION, + // seconds round + '[s]' => 0, + '[ss]' => 0, + ]; + private const INTERVAL_MULTIPLIER = [ + '[h]' => self::HOURS_IN_DAY, + '[hh]' => self::HOURS_IN_DAY, + '[m]' => self::MINUTES_IN_DAY, + '[mm]' => self::MINUTES_IN_DAY, + '[s]' => self::SECONDS_IN_DAY, + '[ss]' => self::SECONDS_IN_DAY, + ]; + + /** @param float|int|numeric-string $value */ + private static function tryInterval(bool &$seekingBracket, string &$block, mixed $value, string $format): void + { + if ($seekingBracket) { + if (str_contains($block, $format)) { + $hours = (string) (int) round( + self::INTERVAL_MULTIPLIER[$format] * $value, + self::INTERVAL_ROUND_PRECISION[$format] + ); + if (strlen($hours) === 1 && in_array($format, self::INTERVAL_LEADING_ZERO, true)) { + $hours = "0$hours"; + } + $block = str_replace($format, $hours, $block); + $seekingBracket = false; + } + } + } + + /** @param float|int $value value to be formatted */ + public static function format(mixed $value, string $format): string + { + // strip off first part containing e.g. [$-F800] or [$USD-409] + // general syntax: [$-] + // language info is in hexadecimal + // strip off chinese part like [DBNum1][$-804] + $format = (string) preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format); + + // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case; + // but we don't want to change any quoted strings + /** @var callable $callable */ + $callable = [self::class, 'setLowercaseCallback']; + $format = (string) preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); + + // Only process the non-quoted blocks for date format characters + + $blocks = explode('"', $format); + foreach ($blocks as $key => &$block) { + if ($key % 2 == 0) { + $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS); + if (!strpos($block, 'A')) { + // 24-hour time format + // when [h]:mm format, the [h] should replace to the hours of the value * 24 + $seekingBracket = true; + self::tryInterval($seekingBracket, $block, $value, '[h]'); + self::tryInterval($seekingBracket, $block, $value, '[hh]'); + self::tryInterval($seekingBracket, $block, $value, '[mm]'); + self::tryInterval($seekingBracket, $block, $value, '[m]'); + self::tryInterval($seekingBracket, $block, $value, '[s]'); + self::tryInterval($seekingBracket, $block, $value, '[ss]'); + $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS24); + } else { + // 12-hour time format + $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS12); + } + } + } + $format = implode('"', $blocks); + + // escape any quoted characters so that DateTime format() will render them correctly + /** @var callable $callback */ + $callback = [self::class, 'escapeQuotesCallback']; + $format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format); + + try { + $dateObj = Date::excelToDateTimeObject($value); + } catch (Throwable) { + return StringHelper::convertToString($value); + } + // If the colon preceding minute had been quoted, as happens in + // Excel 2003 XML formats, m will not have been changed to i above. + // Change it now. + $format = (string) \preg_replace('/\\\:m/', ':i', $format); + $microseconds = (int) $dateObj->format('u'); + if (str_contains($format, ':s.000')) { + $milliseconds = (int) round($microseconds / 1000.0); + if ($milliseconds === 1000) { + $milliseconds = 0; + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + $format = str_replace(':s.000', ':s.' . sprintf('%03d', $milliseconds), $format); + } elseif (str_contains($format, ':s.00')) { + $centiseconds = (int) round($microseconds / 10000.0); + if ($centiseconds === 100) { + $centiseconds = 0; + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + $format = str_replace(':s.00', ':s.' . sprintf('%02d', $centiseconds), $format); + } elseif (str_contains($format, ':s.0')) { + $deciseconds = (int) round($microseconds / 100000.0); + if ($deciseconds === 10) { + $deciseconds = 0; + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + $format = str_replace(':s.0', ':s.' . sprintf('%1d', $deciseconds), $format); + } else { // no fractional second + if ($microseconds >= 500000) { + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + } + + return $dateObj->format($format); + } + + /** @param string[] $matches */ + private static function setLowercaseCallback(array $matches): string + { + return mb_strtolower($matches[0]); + } + + /** @param string[] $matches */ + private static function escapeQuotesCallback(array $matches): string + { + return '\\' . implode('\\', mb_str_split($matches[1], 1, 'UTF-8')); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php new file mode 100644 index 0000000..aba75f4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -0,0 +1,231 @@ +' => $value > $comparisonValue, + '<' => $value < $comparisonValue, + '<=' => $value <= $comparisonValue, + '<>' => $value != $comparisonValue, + '=' => $value == $comparisonValue, + default => $value >= $comparisonValue, + }; + } + + /** + * @param float|int|numeric-string $value value to be formatted + * @param string[] $sections + * + * @return mixed[] + */ + private static function splitFormatForSectionSelection(array $sections, mixed $value): array + { + // Extract the relevant section depending on whether number is positive, negative, or zero? + // Text not supported yet. + // Here is how the sections apply to various values in Excel: + // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT] + // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE] + // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO] + // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] + $sectionCount = count($sections); + // Colour could be a named colour, or a numeric index entry in the colour-palette + $color_regex = '/\[(' . implode('|', Color::NAMED_COLORS) . '|color\s*(\d+))\]/mui'; + $cond_regex = '/\[(>|>=|<|<=|=|<>)([+-]?\d+([.]\d+)?)\]/'; + $colors = ['', '', '', '', '']; + $conditionOperations = ['', '', '', '', '']; + $conditionComparisonValues = [0, 0, 0, 0, 0]; + for ($idx = 0; $idx < $sectionCount; ++$idx) { + if (preg_match($color_regex, $sections[$idx], $matches)) { + if (isset($matches[2])) { + $colors[$idx] = '#' . BIFF8::lookup((int) $matches[2] + 7)['rgb']; + } else { + $colors[$idx] = $matches[0]; + } + $sections[$idx] = (string) preg_replace($color_regex, '', $sections[$idx]); + } + if (preg_match($cond_regex, $sections[$idx], $matches)) { + $conditionOperations[$idx] = $matches[1]; + $conditionComparisonValues[$idx] = $matches[2]; + $sections[$idx] = (string) preg_replace($cond_regex, '', $sections[$idx]); + } + } + $color = $colors[0]; + $format = $sections[0]; + $absval = $value; + switch ($sectionCount) { + case 2: + $absval = abs($value + 0); + if (!self::splitFormatComparison($value, $conditionOperations[0], $conditionComparisonValues[0], '>=', 0)) { + $color = $colors[1]; + $format = $sections[1]; + } + + break; + case 3: + case 4: + $absval = abs($value + 0); + if (!self::splitFormatComparison($value, $conditionOperations[0], $conditionComparisonValues[0], '>', 0)) { + if (self::splitFormatComparison($value, $conditionOperations[1], $conditionComparisonValues[1], '<', 0)) { + $color = $colors[1]; + $format = $sections[1]; + } else { + $color = $colors[2]; + $format = $sections[2]; + } + } + + break; + } + + return [$color, $format, $absval]; + } + + /** + * Convert a value in a pre-defined format to a PHP string. + * + * @param null|array|bool|float|int|RichText|string $value Value to format + * @param string $format Format code: see = self::FORMAT_* for predefined values; + * or can be any valid MS Excel custom format string + * @param null|array|callable $callBack Callback function for additional formatting of string + * @param bool $lessFloatPrecision If true, unstyled floats will be converted to a more human-friendly but less computationally accurate value + * + * @return string Formatted string + */ + public static function toFormattedString($value, string $format, null|array|callable $callBack = null, bool $lessFloatPrecision = false): string + { + while (is_array($value)) { + $value = array_shift($value); + } + if (is_bool($value)) { + return $value ? Calculation::getTRUE() : Calculation::getFALSE(); + } + // For now we do not treat strings in sections, although section 4 of a format code affects strings + // Process a single block format code containing @ for text substitution + $formatx = str_replace('\"', self::QUOTE_REPLACEMENT, $format); + if (preg_match(self::SECTION_SPLIT, $format) === 0 && preg_match(self::SYMBOL_AT, $formatx) === 1) { + if (!str_contains($format, '"')) { + return str_replace('@', StringHelper::convertToString($value, lessFloatPrecision: $lessFloatPrecision), $format); + } + //escape any dollar signs on the string, so they are not replaced with an empty value + $value = str_replace( + ['$', '"'], + ['\$', self::QUOTE_REPLACEMENT], + StringHelper::convertToString($value, lessFloatPrecision: $lessFloatPrecision) + ); + + return str_replace( + ['"', self::QUOTE_REPLACEMENT], + ['', '"'], + preg_replace(self::SYMBOL_AT, $value, $formatx) ?? $value + ); + } + + // If we have a text value, return it "as is" + if (!is_numeric($value)) { + return StringHelper::convertToString($value, lessFloatPrecision: $lessFloatPrecision); + } + + // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, + // it seems to round numbers to a total of 10 digits. + if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) { + if (is_float($value) && $lessFloatPrecision) { + return self::adjustSeparators((string) $value); + } + + return self::adjustSeparators( + StringHelper::convertToString($value, lessFloatPrecision: $lessFloatPrecision) + ); + } + + // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc + $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format); + + $format = (string) preg_replace_callback( + '/(["])(?:(?=(\\\?))\2.)*?\1/u', + fn (array $matches): string => str_replace('.', chr(0x00), $matches[0]), + $format + ); + + // Convert any other escaped characters to quoted strings, e.g. (\T to "T") + $format = (string) preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); + + // Get the sections, there can be up to four sections, separated with a semicolon (but only if not a quoted literal) + $sections = preg_split(self::SECTION_SPLIT, $format) ?: []; + + [$colors, $format, $value] = self::splitFormatForSectionSelection($sections, $value); + + // In Excel formats, "_" is used to add spacing, + // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space + /** @var string */ + $temp = $format; + $format = (string) preg_replace('/_.?/ui', ' ', $temp); + + // Let's begin inspecting the format and converting the value to a formatted string + if ( + // Check for date/time characters (not inside quotes) + (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format)) + // Look out for Currency formats Issue 4124 + && !(preg_match('/\[\$[A-Z]{3}\]/miu', $format)) + // A date/time with a decimal time shouldn't have a digit placeholder before the decimal point + && (preg_match('/[0\?#]\.(?![^\[]*\])/miu', $format) === 0) + ) { + // datetime format + /** @var float|int */ + $temp = $value; + $value = DateFormatter::format($temp, $format); + } else { + if (str_starts_with($format, '"') && str_ends_with($format, '"') && substr_count($format, '"') === 2) { + $value = substr($format, 1, -1); + } elseif (preg_match('/[0#, ]%/', $format)) { + // % number format - avoid weird '-0' problem + /** @var float */ + $temp = $value; + $value = PercentageFormatter::format(0 + (float) $temp, $format); + } else { + /** @var float|int|numeric-string */ + $temp = $value; + $value = NumberFormatter::format($temp, $format); + } + } + + // Additional formatting provided by callback function + if (is_callable($callBack)) { + $value = $callBack($value, $colors); + } + /** @var string $value */ + + return str_replace(chr(0x00), '.', $value); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php new file mode 100644 index 0000000..7affc21 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -0,0 +1,70 @@ + 0); + + return [ + implode('.', $masks), + implode('.', array_reverse($postDecimalMasks)), + ]; + } + + private static function processComplexNumberFormatMask(mixed $number, string $mask): string + { + /** @var string $result */ + $result = $number; + $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE); + + if ($maskingBlockCount > 1) { + $maskingBlocks = array_reverse($maskingBlocks[0]); + + $offset = 0; + foreach ($maskingBlocks as $block) { + $size = strlen($block[0]); + $divisor = 10 ** $size; + $offset = $block[1]; + + /** @var float $numberFloat */ + $numberFloat = $number; + $blockValue = sprintf("%0{$size}d", fmod($numberFloat, $divisor)); + $number = floor($numberFloat / $divisor); + $mask = substr_replace($mask, $blockValue, $offset, $size); + } + /** @var string $numberString */ + $numberString = $number; + if ($number > 0) { + $mask = substr_replace($mask, $numberString, $offset, 0); + } + $result = $mask; + } + + return self::makeString($result); + } + + private static function complexNumberFormatMask(mixed $number, string $mask, bool $splitOnPoint = true): string + { + /** @var float $numberFloat */ + $numberFloat = $number; + if ($splitOnPoint) { + $masks = explode('.', $mask); + if (count($masks) <= 2) { + $decmask = $masks[1] ?? ''; + $decpos = substr_count($decmask, '0'); + $numberFloat = round($numberFloat, $decpos); + } + } + $sign = ($numberFloat < 0.0) ? '-' : ''; + $number = self::f2s(abs($numberFloat)); + + if ($splitOnPoint && str_contains($mask, '.') && str_contains($number, '.')) { + $numbers = explode('.', $number); + $masks = explode('.', $mask); + if (count($masks) > 2) { + $masks = self::mergeComplexNumberFormatMasks($numbers, $masks); + } + /** @var string[] $masks */ + $integerPart = self::complexNumberFormatMask($numbers[0], $masks[0], false); + $numlen = strlen($numbers[1]); + $msklen = strlen($masks[1]); + if ($numlen < $msklen) { + $numbers[1] .= str_repeat('0', $msklen - $numlen); + } + $decimalPart = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false)); + $decimalPart = substr($decimalPart, 0, $msklen); + + return "{$sign}{$integerPart}.{$decimalPart}"; + } + + if (strlen($number) < strlen($mask)) { + $number = str_repeat('0', strlen($mask) - strlen($number)) . $number; + } + $result = self::processComplexNumberFormatMask($number, $mask); + + return "{$sign}{$result}"; + } + + public static function f2s(float $f): string + { + return self::floatStringConvertScientific((string) $f); + } + + public static function floatStringConvertScientific(string $s): string + { + // convert only normalized form of scientific notation: + // optional sign, single digit 1-9, + // decimal point and digits (allowed to be omitted), + // E (e permitted), optional sign, one or more digits + if (preg_match('/^([+-])?([1-9])([.]([0-9]+))?[eE]([+-]?[0-9]+)$/', $s, $matches) === 1) { + $exponent = (int) $matches[5]; + $sign = ($matches[1] === '-') ? '-' : ''; + if ($exponent >= 0) { + $exponentPlus1 = $exponent + 1; + $out = $matches[2] . $matches[4]; + $len = strlen($out); + if ($len < $exponentPlus1) { + $out .= str_repeat('0', $exponentPlus1 - $len); + } + $out = substr($out, 0, $exponentPlus1) . ((strlen($out) === $exponentPlus1) ? '' : ('.' . substr($out, $exponentPlus1))); + $s = "$sign$out"; + } else { + $s = $sign . '0.' . str_repeat('0', -$exponent - 1) . $matches[2] . $matches[4]; + } + } + + return $s; + } + + /** @param string[] $matches */ + private static function formatStraightNumericValue(mixed $value, string $format, array $matches, bool $useThousands): string + { + /** @var float $valueFloat */ + $valueFloat = $value; + $left = $matches[1]; + $dec = $matches[2]; + $right = $matches[3]; + + // minimum width of formatted number (including dot) + $minWidth = strlen($left) + strlen($dec) + strlen($right); + if ($useThousands) { + $value = number_format( + $valueFloat, + strlen($right), + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + + return self::pregReplace(self::NUMBER_REGEX, $value, $format); + } + + if (preg_match('/[0#]E[+-]0/i', $format)) { + // Scientific format + $decimals = strlen($right); + $size = $decimals + 3; + + return sprintf("%{$size}.{$decimals}E", $valueFloat); + } + if (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { + if ($valueFloat == floor($valueFloat) && substr_count($format, '.') === 1) { + $value *= 10 ** strlen(explode('.', $format)[1]); //* @phpstan-ignore-line + } + + $result = self::complexNumberFormatMask($value, $format); + if (str_contains($result, 'E')) { + // This is a hack and doesn't match Excel. + // It will, at least, be an accurate representation, + // even if formatted incorrectly. + // This is needed for absolute values >=1E18. + $result = self::f2s($valueFloat); + } + + return $result; + } + + $sprintf_pattern = "%0$minWidth." . strlen($right) . 'F'; + + /** @var float $valueFloat */ + $valueFloat = $value; + $value = self::adjustSeparators(sprintf($sprintf_pattern, round($valueFloat, strlen($right)))); + + return self::pregReplace(self::NUMBER_REGEX, $value, $format); + } + + /** @param float|int|numeric-string $value value to be formatted */ + public static function format(mixed $value, string $format): string + { + // The "_" in this string has already been stripped out, + // so this test is never true. Furthermore, testing + // on Excel shows this format uses Euro symbol, not "EUR". + // if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) { + // return 'EUR ' . sprintf('%1.2f', $value); + // } + + $baseFormat = $format; + + $useThousands = self::areThousandsRequired($format); + $scale = self::scaleThousandsMillions($format); + + if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $format)) { + // It's a dirty hack; but replace # and 0 digit placeholders with ? + $format = (string) preg_replace('/[#0]+\//', '?/', $format); + $format = (string) preg_replace('/\/[#0]+/', '/?', $format); + $value = FractionFormatter::format($value, $format); + } else { + // Handle the number itself + // scale number + $value = $value / $scale; + $paddingPlaceholder = (str_contains($format, '?')); + + // Replace # or ? with 0 + $format = self::pregReplace('/[\#\?](?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format); + // Remove locale code [$-###] for an LCID + $format = self::pregReplace('/\[\$\-.*\]/', '', $format); + + $n = '/\[[^\]]+\]/'; + $m = self::pregReplace($n, '', $format); + + // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols + $format = self::makeString(str_replace(['"', '*'], '', $format)); + if (preg_match(self::NUMBER_REGEX, $m, $matches)) { + // There are placeholders for digits, so inject digits from the value into the mask + $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands); + if ($paddingPlaceholder === true) { + $value = self::padValue($value, $baseFormat); + } + } elseif ($format !== NumberFormat::FORMAT_GENERAL) { + // Yes, I know that this is basically just a hack; + // if there's no placeholders for digits, just return the format mask "as is" + $value = self::makeString(str_replace('?', '', $format)); + } + } + + if (preg_match('/\[\$(.*)\]/u', $format, $m)) { + // Currency or Accounting + $value = preg_replace('/-0+(( |\xc2\xa0))?\[/', '- [', (string) $value) ?? $value; + $currencyCode = $m[1]; + [$currencyCode] = explode('-', $currencyCode); + if ($currencyCode == '') { + $currencyCode = StringHelper::getCurrencyCode(); + } + $value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value); + } + + if ( + (str_contains((string) $value, '0.')) + && ((str_contains($baseFormat, '#.')) || (str_contains($baseFormat, '?.'))) + ) { + $value = preg_replace('/(\b)0\.|([^\d])0\./', '${2}.', (string) $value); + } + + return (string) $value; + } + + /** @param mixed[]|string $value */ + private static function makeString(array|string $value): string + { + return is_array($value) ? '' : "$value"; + } + + private static function pregReplace(string $pattern, string $replacement, string $subject): string + { + return self::makeString(preg_replace($pattern, $replacement, $subject) ?? ''); + } + + public static function padValue(string $value, string $baseFormat): string + { + $preDecimal = $postDecimal = ''; + $pregArray = preg_split('/\.(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu', $baseFormat . '.?'); + if (is_array($pregArray)) { + $preDecimal = $pregArray[0]; + $postDecimal = $pregArray[1] ?? ''; + } + + $length = strlen($value); + if (str_contains($postDecimal, '?')) { + $value = str_pad(rtrim($value, '0. '), $length, ' ', STR_PAD_RIGHT); + } + if (str_contains($preDecimal, '?')) { + $value = str_pad(ltrim($value, '0, '), $length, ' ', STR_PAD_LEFT); + } + + return $value; + } + + /** + * Find out if we need thousands separator + * This is indicated by a comma enclosed by a digit placeholders: #, 0 or ? + */ + public static function areThousandsRequired(string &$format): bool + { + $useThousands = (bool) preg_match('/([#\?0]),([#\?0])/', $format); + if ($useThousands) { + $format = self::pregReplace('/([#\?0]),([#\?0])/', '${1}${2}', $format); + } + + return $useThousands; + } + + /** + * Scale thousands, millions,... + * This is indicated by a number of commas after a digit placeholder: #, or 0.0,, or ?,. + */ + public static function scaleThousandsMillions(string &$format): int + { + $scale = 1; // same as no scale + if (preg_match('/(#|0|\?)(,+)/', $format, $matches)) { + $scale = 1000 ** strlen($matches[2]); + // strip the commas + $format = self::pregReplace('/([#\?0]),+/', '${1}', $format); + } + + return $scale; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php new file mode 100644 index 0000000..3c0575b --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -0,0 +1,48 @@ + 0); + $replacement = "0{$wholePartSize}.{$decimalPartSize}"; + $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}F{$placeHolders}", $format); + + /** @var float $valueFloat */ + $valueFloat = $value; + + return self::adjustSeparators(sprintf($mask, round($valueFloat, $decimalPartSize))); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Accounting.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Accounting.php new file mode 100644 index 0000000..0c90a71 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Accounting.php @@ -0,0 +1,50 @@ +fullLocale, NumberFormatter::CURRENCY_ACCOUNTING); + $mask = $formatter->format($this->stripLeadingRLM); + if ($this->decimals === 0) { + $mask = (string) preg_replace('/\.0+/miu', '', $mask); + } + + return str_replace('¤', $this->formatCurrencyCode(), $mask); + } + + public static function icuVersion(): float + { + [$major, $minor] = explode('.', INTL_ICU_VERSION); + + return (float) "{$major}.{$minor}"; + } + + private function formatCurrencyCode(): string + { + if ($this->locale === null) { + return $this->currencyCode . '*'; + } + + return "[\${$this->currencyCode}-{$this->locale}]"; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Currency.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Currency.php new file mode 100644 index 0000000..e5828a1 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Currency.php @@ -0,0 +1,10 @@ +setCurrencyCode($currencyCode); + $this->setThousandsSeparator($thousandsSeparator); + $this->setDecimals($decimals); + $this->setCurrencySymbolPosition($currencySymbolPosition); + $this->setCurrencySymbolSpacing($currencySymbolSpacing); + $this->setLocale($locale); + $this->stripLeadingRLM = $stripLeadingRLM; + $this->negative = $negative; + } + + public function setCurrencyCode(string $currencyCode): void + { + $this->currencyCode = $currencyCode; + } + + public function setCurrencySymbolPosition(bool $currencySymbolPosition = self::LEADING_SYMBOL): void + { + $this->currencySymbolPosition = $currencySymbolPosition; + } + + public function setCurrencySymbolSpacing(bool $currencySymbolSpacing = self::SYMBOL_WITHOUT_SPACING): void + { + $this->currencySymbolSpacing = $currencySymbolSpacing; + } + + public function setStripLeadingRLM(bool $stripLeadingRLM): void + { + $this->stripLeadingRLM = $stripLeadingRLM; + } + + public function setNegative(CurrencyNegative $negative): void + { + $this->negative = $negative; + } + + protected function getLocaleFormat(): string + { + $formatter = new Locale($this->fullLocale, NumberFormatter::CURRENCY); + $mask = $formatter->format($this->stripLeadingRLM); + if ($this->decimals === 0) { + $mask = (string) preg_replace('/\.0+/miu', '', $mask); + } + + return str_replace('¤', $this->formatCurrencyCode(), $mask); + } + + private function formatCurrencyCode(): string + { + if ($this->locale === null) { + return $this->currencyCode; + } + + return "[\${$this->currencyCode}-{$this->locale}]"; + } + + public function format(): string + { + if ($this->localeFormat !== null) { + return $this->localeFormat; + } + $symbolWithSpacing = $this->overrideSpacing ?? ($this->currencySymbolSpacing === self::SYMBOL_WITH_SPACING); + $negative = $this->overrideNegative ?? $this->negative; + + // format if positive + $format = '_('; + if ($this->currencySymbolPosition === self::LEADING_SYMBOL) { + $format .= '"' . $this->currencyCode . '"'; + if (preg_match('/^[A-Z]{3}$/i', $this->currencyCode) === 1) { + $format .= $this->spaceOrNbsp; + } + if (preg_match('/^[A-Z]{3}$/i', $this->currencyCode) === 1) { + $format .= $this->spaceOrNbsp; + } + if ($symbolWithSpacing) { + $format .= '*' . $this->spaceOrNbsp; + } + } + $format .= $this->thousandsSeparator ? '#,##0' : '0'; + if ($this->decimals > 0) { + $format .= '.' . str_repeat('0', $this->decimals); + } + if ($this->currencySymbolPosition === self::TRAILING_SYMBOL) { + if ($symbolWithSpacing) { + $format .= $this->spaceOrNbsp; + } elseif (preg_match('/^[A-Z]{3}$/i', $this->currencyCode) === 1) { + $format .= $this->spaceOrNbsp; + } + $format .= '[$' . $this->currencyCode . ']'; + } + $format .= '_)'; + + // format if negative + $format .= ';_('; + $format .= $negative->color(); + $negativeStart = $negative->start(); + if ($this->currencySymbolPosition === self::LEADING_SYMBOL) { + if ($negativeStart === '-' && !$symbolWithSpacing) { + $format .= $negativeStart; + } + $format .= '"' . $this->currencyCode . '"'; + if (preg_match('/^[A-Z]{3}$/i', $this->currencyCode) === 1) { + $format .= $this->spaceOrNbsp; + } + if ($symbolWithSpacing) { + $format .= '*' . $this->spaceOrNbsp; + } + if ($negativeStart === '\(' || ($symbolWithSpacing && $negativeStart === '-')) { + $format .= $negativeStart; + } + } else { + $format .= $negative->start(); + } + $format .= $this->thousandsSeparator ? '#,##0' : '0'; + if ($this->decimals > 0) { + $format .= '.' . str_repeat('0', $this->decimals); + } + $format .= $negative->end(); + if ($this->currencySymbolPosition === self::TRAILING_SYMBOL) { + if ($symbolWithSpacing) { + // Do nothing - I can't figure out how to get + // everything to align if I put any kind of space here. + //$format .= "\u{2009}"; + } elseif (preg_match('/^[A-Z]{3}$/i', $this->currencyCode) === 1) { + $format .= $this->spaceOrNbsp; + } + $format .= '[$' . $this->currencyCode . ']'; + } + if ($this->currencySymbolPosition === self::TRAILING_SYMBOL) { + $format .= '_)'; + } elseif ($symbolWithSpacing && $negativeStart === '-') { + $format .= ' '; + } + // format if zero + $format .= ';_('; + if ($this->currencySymbolPosition === self::LEADING_SYMBOL) { + $format .= '"' . $this->currencyCode . '"'; + } + if ($symbolWithSpacing) { + if ($this->currencySymbolPosition === self::LEADING_SYMBOL) { + $format .= '*' . $this->spaceOrNbsp; + } + $format .= '"-"'; + if ($this->decimals > 0) { + $format .= str_repeat('?', $this->decimals); + } + } else { + if (preg_match('/^[A-Z]{3}$/i', $this->currencyCode) === 1) { + $format .= $this->spaceOrNbsp; + } + $format .= '0'; + if ($this->decimals > 0) { + $format .= '.' . str_repeat('0', $this->decimals); + } + } + if ($this->currencySymbolPosition === self::TRAILING_SYMBOL) { + if ($symbolWithSpacing) { + $format .= $this->spaceOrNbsp; + } + $format .= '[$' . $this->currencyCode . ']'; + } + $format .= '_)'; + // format if text + $format .= ';_(@_)'; + + return $format; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/CurrencyNegative.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/CurrencyNegative.php new file mode 100644 index 0000000..07e368f --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/CurrencyNegative.php @@ -0,0 +1,35 @@ + '-', + self::parentheses, self::redParentheses => '\(', + }; + } + + public function end(): string + { + return match ($this) { + self::minus, self::redMinus => '', + self::parentheses, self::redParentheses => '\)', + }; + } + + public function color(): string + { + return match ($this) { + self::redParentheses, self::redMinus => '[Red]', + self::parentheses, self::minus => '', + }; + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Date.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Date.php new file mode 100644 index 0000000..61ac117 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Date.php @@ -0,0 +1,125 @@ +separators = $this->padSeparatorArray( + is_array($separators) ? $separators : [$separators], + count($formatBlocks) - 1 + ); + $this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks); + } + + private function mapFormatBlocks(string $value): string + { + // Any date masking codes are returned as lower case values + if (in_array(mb_strtolower($value), self::DATE_BLOCKS, true)) { + return mb_strtolower($value); + } + + // Wrap any string literals in quotes, so that they're clearly defined as string literals + return $this->wrapLiteral($value); + } + + public function format(): string + { + return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/DateTime.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/DateTime.php new file mode 100644 index 0000000..5f57a24 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/DateTime.php @@ -0,0 +1,46 @@ + + */ + protected array $formatBlocks; + + /** + * @param null|string|string[] $separators + * If you want to use only a single format block, then pass a null as the separator argument + * @param DateTimeWizard|string ...$formatBlocks + */ + public function __construct($separators, ...$formatBlocks) + { + $this->separators = $this->padSeparatorArray( + is_array($separators) ? $separators : [$separators], //* @phpstan-ignore-line + count($formatBlocks) - 1 + ); + $this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks); + } + + private function mapFormatBlocks(DateTimeWizard|string $value): string + { + // Any date masking codes are returned as lower case values + if ($value instanceof DateTimeWizard) { + return $value->__toString(); + } + + // Wrap any string literals in quotes, so that they're clearly defined as string literals + return $this->wrapLiteral($value); + } + + public function format(): string + { + return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/DateTimeWizard.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/DateTimeWizard.php new file mode 100644 index 0000000..1fdb648 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/DateTimeWizard.php @@ -0,0 +1,51 @@ += "; + + /** + * @param string[] $separators + * + * @return string[] + */ + protected function padSeparatorArray(array $separators, int $count): array + { + $lastSeparator = (string) array_pop($separators); + + return $separators + array_fill(0, $count, $lastSeparator); + } + + protected function escapeSingleCharacter(string $value): string + { + if (str_contains(self::NO_ESCAPING_NEEDED, $value)) { + return $value; + } + + return "\\{$value}"; + } + + protected function wrapLiteral(string $value): string + { + if (mb_strlen($value, 'UTF-8') === 1) { + return $this->escapeSingleCharacter($value); + } + + // Wrap any other string literals in quotes, so that they're clearly defined as string literals + return '"' . str_replace('"', '""', $value) . '"'; + } + + protected function intersperse(string $formatBlock, ?string $separator): string + { + return "{$formatBlock}{$separator}"; + } + + public function __toString(): string + { + return $this->format(); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Duration.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Duration.php new file mode 100644 index 0000000..b81f77a --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Duration.php @@ -0,0 +1,153 @@ + self::DAYS_DURATION, + self::HOURS_DURATION => self::HOURS_SHORT, + self::MINUTES_DURATION => self::MINUTES_LONG, + self::SECONDS_DURATION => self::SECONDS_LONG, + ]; + + protected const DURATION_DEFAULTS = [ + self::HOURS_LONG => self::HOURS_DURATION, + self::HOURS_SHORT => self::HOURS_DURATION, + self::MINUTES_LONG => self::MINUTES_DURATION, + self::MINUTES_SHORT => self::MINUTES_DURATION, + self::SECONDS_LONG => self::SECONDS_DURATION, + self::SECONDS_SHORT => self::SECONDS_DURATION, + ]; + + public const SEPARATOR_COLON = ':'; + public const SEPARATOR_SPACE_NONBREAKING = "\u{a0}"; + public const SEPARATOR_SPACE = ' '; + + public const DURATION_DEFAULT = [ + self::HOURS_DURATION, + self::MINUTES_LONG, + self::SECONDS_LONG, + ]; + + /** + * @var string[] + */ + protected array $separators; + + /** + * @var string[] + */ + protected array $formatBlocks; + + protected bool $durationIsSet = false; + + /** + * @param null|string|string[] $separators + * If you want to use the same separator for all format blocks, then it can be passed as a string literal; + * if you wish to use different separators, then they should be passed as an array. + * If you want to use only a single format block, then pass a null as the separator argument + */ + public function __construct($separators = self::SEPARATOR_COLON, string ...$formatBlocks) + { + $separators ??= self::SEPARATOR_COLON; + $formatBlocks = (count($formatBlocks) === 0) ? self::DURATION_DEFAULT : $formatBlocks; + + $this->separators = $this->padSeparatorArray( + is_array($separators) ? $separators : [$separators], + count($formatBlocks) - 1 + ); + $this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks); + + if ($this->durationIsSet === false) { + // We need at least one duration mask, so if none has been set we change the first mask element + // to a duration. + $this->formatBlocks[0] = self::DURATION_DEFAULTS[mb_strtolower($this->formatBlocks[0])]; + } + } + + private function mapFormatBlocks(string $value): string + { + // Any duration masking codes are returned as lower case values + if (in_array(mb_strtolower($value), self::DURATION_BLOCKS, true)) { + if (array_key_exists(mb_strtolower($value), self::DURATION_MASKS)) { + if ($this->durationIsSet) { + // We should only have a single duration mask, the first defined in the mask set, + // so convert any additional duration masks to standard time masks. + $value = self::DURATION_MASKS[mb_strtolower($value)]; + } + $this->durationIsSet = true; + } + + return mb_strtolower($value); + } + + // Wrap any string literals in quotes, so that they're clearly defined as string literals + return $this->wrapLiteral($value); + } + + public function format(): string + { + return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators)); + } +} diff --git a/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php new file mode 100644 index 0000000..06669d4 --- /dev/null +++ b/application/third_party/PhpSpreadsheet-5.3.0/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php @@ -0,0 +1,35 @@ +[a-z]{2})([-_](?P tags. + * + * ==== IMPORTANT ==== + * Enabling this for documents you do not trust (e.g. arbitrary remote html + * pages) is a security risk. Embedded scripts are run with the same level of + * system access available to dompdf. Set this option to false (recommended) + * if you wish to process untrusted documents. + * + * This setting may increase the risk of system exploit. Do not change + * this settings without understanding the consequences. Additional + * documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + private $isPhpEnabled = false; + + /** + * Enable remote file access + * + * If this setting is set to true, DOMPDF will access remote sites for + * images and CSS files as required. + * + * ==== IMPORTANT ==== + * This can be a security risk, in particular in combination with isPhpEnabled and + * allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...); + * This allows anonymous users to download legally doubtful internet content which on + * tracing back appears to being downloaded by your server, or allows malicious php code + * in remote html pages to be executed by your server with your account privileges. + * + * This setting may increase the risk of system exploit. Do not change + * this settings without understanding the consequences. Additional + * documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + private $isRemoteEnabled = false; + + /** + * List of allowed remote hosts + * + * Each value of the array must be a valid hostname. + * + * This will be used to filter which resources can be loaded in combination with + * isRemoteEnabled. If isRemoteEnabled is FALSE, then this will have no effect. + * + * Leave to NULL to allow any remote host. + * + * @var array|null + */ + private $allowedRemoteHosts = null; + + /** + * Enable PDF/A-3 compliance mode + * + * ==== EXPERIMENTAL ==== + * This feature is currently only supported with the CPDF backend and will + * have no effect if used with any other. + * + * Currently this mode only takes care of adding the necessary metadata, output intents, etc. + * It does not enforce font embedding, it's up to you to embed the fonts you plan on using. + * + * @var bool + */ + private $isPdfAEnabled = false; + + /** + * Enable inline JavaScript + * + * If this setting is set to true then DOMPDF will automatically insert + * JavaScript code contained within + * tags as written into the PDF. + * + * NOTE: This is PDF-based JavaScript to be executed by the PDF viewer, + * not browser-based JavaScript executed by Dompdf. + * + * @var bool + */ + private $isJavascriptEnabled = true; + + /** + * Use the HTML5 Lib parser + * + * @deprecated + * @var bool + */ + private $isHtml5ParserEnabled = true; + + /** + * Whether to enable font subsetting or not. + * + * @var bool + */ + private $isFontSubsettingEnabled = true; + + /** + * @var bool + */ + private $debugPng = false; + + /** + * @var bool + */ + private $debugKeepTemp = false; + + /** + * @var bool + */ + private $debugCss = false; + + /** + * @var bool + */ + private $debugLayout = false; + + /** + * @var bool + */ + private $debugLayoutLines = true; + + /** + * @var bool + */ + private $debugLayoutBlocks = true; + + /** + * @var bool + */ + private $debugLayoutInline = true; + + /** + * @var bool + */ + private $debugLayoutPaddingBox = true; + + /** + * The PDF rendering backend to use + * + * Valid settings are 'PDFLib', 'CPDF', 'GD', and 'auto'. 'auto' will + * look for PDFLib and use it if found, or if not it will fall back on + * CPDF. 'GD' renders PDFs to graphic files. {@link Dompdf\CanvasFactory} + * ultimately determines which rendering class to instantiate + * based on this setting. + * + * @var string + */ + private $pdfBackend = "CPDF"; + + /** + * PDFlib license key + * + * If you are using a licensed, commercial version of PDFlib, specify + * your license key here. If you are using PDFlib-Lite or are evaluating + * the commercial version of PDFlib, comment out this setting. + * + * @link http://www.pdflib.com + * + * If pdflib present in web server and auto or selected explicitly above, + * a real license code must exist! + * + * @var string + */ + private $pdflibLicense = ""; + + /** + * HTTP context created with stream_context_create() + * Will be used for file_get_contents + * + * @link https://www.php.net/manual/context.php + * + * @var resource + */ + private $httpContext; + + /** + * @param array $attributes + */ + public function __construct(?array $attributes = null) + { + $rootDir = realpath(__DIR__ . "/../"); + $this->setChroot(array($rootDir)); + $this->setRootDir($rootDir); + $this->setTempDir(sys_get_temp_dir()); + $this->setFontDir($rootDir . "/lib/fonts"); + $this->setFontCache($this->getFontDir()); + + $ver = ""; + $versionFile = realpath(__DIR__ . '/../VERSION'); + if (($version = file_get_contents($versionFile)) !== false) { + $version = trim($version); + if ($version !== '$Format:<%h>$') { + $ver = "/$version"; + } + } + $this->setHttpContext([ + "http" => [ + "follow_location" => false, + "user_agent" => "Dompdf$ver https://github.com/dompdf/dompdf" + ] + ]); + + $this->setAllowedProtocols(["data://", "file://", "http://", "https://"]); + + $this->setArtifactPathValidation([$this, "validateArtifactPath"]); + + if (null !== $attributes) { + $this->set($attributes); + } + } + + /** + * @param array|string $attributes + * @param null|mixed $value + * @return $this + */ + public function set($attributes, $value = null) + { + if (!is_array($attributes)) { + $attributes = [$attributes => $value]; + } + + foreach ($attributes as $key => $value) { + $methodForMatch = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + $methodForCall = "set" . ucfirst($methodForMatch); + + if ($methodForMatch === 'enablePhp') { + $methodForCall = "setIsPhpEnabled"; + } elseif ($methodForMatch === 'enableRemote') { + $methodForCall = "setIsRemoteEnabled"; + } elseif ($methodForMatch === 'enablePdfA') { + $methodForCall = "setIsPdfAEnabled"; + } elseif ($methodForMatch === 'enableJavascript') { + $methodForCall = "setIsJavascriptEnabled"; + } elseif ($methodForMatch === 'enableHtml5Parser') { + $methodForCall = "setIsHtml5ParserEnabled"; + } elseif ($methodForMatch === 'enableFontSubsetting') { + $methodForCall = "setIsFontSubsettingEnabled"; + } + + if (method_exists($this, $methodForCall)) { + $this->{$methodForCall}($value); + } + } + + return $this; + } + + /** + * @param string $key + * @return mixed + */ + public function get($key) + { + $methodForMatch = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + $methodForCall = "get" . ucfirst($methodForMatch); + + if ($methodForMatch === 'enablePhp') { + $methodForCall = "getIsPhpEnabled"; + } elseif ($methodForMatch === 'enableRemote') { + $methodForCall = "getIsRemoteEnabled"; + } elseif ($methodForMatch === 'enablePdfA') { + $methodForCall = "getIsPdfAEnabled"; + } elseif ($methodForMatch === 'enableJavascript') { + $methodForCall = "getIsJavascriptEnabled"; + } elseif ($methodForMatch === 'enableHtml5Parser') { + $methodForCall = "getIsHtml5ParserEnabled"; + } elseif ($methodForMatch === 'enableFontSubsetting') { + $methodForCall = "getIsFontSubsettingEnabled"; + } + + if (method_exists($this, $methodForCall)) { + return $this->{$methodForCall}(); + } + + return null; + } + + /** + * @param string $pdfBackend + * @return $this + */ + public function setPdfBackend($pdfBackend) + { + $this->pdfBackend = $pdfBackend; + return $this; + } + + /** + * @return string + */ + public function getPdfBackend() + { + return $this->pdfBackend; + } + + /** + * @param string $pdflibLicense + * @return $this + */ + public function setPdflibLicense($pdflibLicense) + { + $this->pdflibLicense = $pdflibLicense; + return $this; + } + + /** + * @return string + */ + public function getPdflibLicense() + { + return $this->pdflibLicense; + } + + /** + * @param array|string $chroot + * @return $this + */ + public function setChroot($chroot, $delimiter = ',') + { + if (is_string($chroot)) { + $this->chroot = explode($delimiter, $chroot); + } elseif (is_array($chroot)) { + $this->chroot = $chroot; + } + return $this; + } + + /** + * @return array + */ + public function getAllowedProtocols() + { + return $this->allowedProtocols; + } + + /** + * @param array $allowedProtocols The protocols to allow, as an array + * formatted as ["protocol://" => ["rules" => [callable]], ...] + * or ["protocol://", ...] + * + * @return $this + */ + public function setAllowedProtocols(array $allowedProtocols) + { + $protocols = []; + foreach ($allowedProtocols as $protocol => $config) { + if (is_string($protocol)) { + $protocols[$protocol] = []; + if (is_array($config)) { + $protocols[$protocol] = $config; + } + } elseif (is_string($config)) { + $protocols[$config] = []; + } + } + $this->allowedProtocols = []; + foreach ($protocols as $protocol => $config) { + $this->addAllowedProtocol($protocol, ...($config["rules"] ?? [])); + } + return $this; + } + + /** + * Adds a new protocol to the allowed protocols collection + * + * @param string $protocol The scheme to add (e.g. "http://") + * @param callable $rule A callable that validates the protocol + * @return $this + */ + public function addAllowedProtocol(string $protocol, callable ...$rules) + { + $protocol = strtolower($protocol); + if (empty($rules)) { + $rules = []; + switch ($protocol) { + case "data://": + break; + case "file://": + $rules[] = [$this, "validateLocalUri"]; + break; + case "http://": + case "https://": + $rules[] = [$this, "validateRemoteUri"]; + break; + case "phar://": + $rules[] = [$this, "validatePharUri"]; + break; + } + } + $this->allowedProtocols[$protocol] = ["rules" => $rules]; + return $this; + } + + /** + * @return array + */ + public function getArtifactPathValidation() + { + return $this->artifactPathValidation; + } + + /** + * @param callable $validator + * @return $this + */ + public function setArtifactPathValidation($validator) + { + $this->artifactPathValidation = $validator; + return $this; + } + + /** + * @return array + */ + public function getChroot() + { + $chroot = []; + if (is_array($this->chroot)) { + $chroot = $this->chroot; + } + return $chroot; + } + + /** + * @param boolean $debugCss + * @return $this + */ + public function setDebugCss($debugCss) + { + $this->debugCss = $debugCss; + return $this; + } + + /** + * @return boolean + */ + public function getDebugCss() + { + return $this->debugCss; + } + + /** + * @param boolean $debugKeepTemp + * @return $this + */ + public function setDebugKeepTemp($debugKeepTemp) + { + $this->debugKeepTemp = $debugKeepTemp; + return $this; + } + + /** + * @return boolean + */ + public function getDebugKeepTemp() + { + return $this->debugKeepTemp; + } + + /** + * @param boolean $debugLayout + * @return $this + */ + public function setDebugLayout($debugLayout) + { + $this->debugLayout = $debugLayout; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayout() + { + return $this->debugLayout; + } + + /** + * @param boolean $debugLayoutBlocks + * @return $this + */ + public function setDebugLayoutBlocks($debugLayoutBlocks) + { + $this->debugLayoutBlocks = $debugLayoutBlocks; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutBlocks() + { + return $this->debugLayoutBlocks; + } + + /** + * @param boolean $debugLayoutInline + * @return $this + */ + public function setDebugLayoutInline($debugLayoutInline) + { + $this->debugLayoutInline = $debugLayoutInline; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutInline() + { + return $this->debugLayoutInline; + } + + /** + * @param boolean $debugLayoutLines + * @return $this + */ + public function setDebugLayoutLines($debugLayoutLines) + { + $this->debugLayoutLines = $debugLayoutLines; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutLines() + { + return $this->debugLayoutLines; + } + + /** + * @param boolean $debugLayoutPaddingBox + * @return $this + */ + public function setDebugLayoutPaddingBox($debugLayoutPaddingBox) + { + $this->debugLayoutPaddingBox = $debugLayoutPaddingBox; + return $this; + } + + /** + * @return boolean + */ + public function getDebugLayoutPaddingBox() + { + return $this->debugLayoutPaddingBox; + } + + /** + * @param boolean $debugPng + * @return $this + */ + public function setDebugPng($debugPng) + { + $this->debugPng = $debugPng; + return $this; + } + + /** + * @return boolean + */ + public function getDebugPng() + { + return $this->debugPng; + } + + /** + * @param string $defaultFont + * @return $this + */ + public function setDefaultFont($defaultFont) + { + if (!($defaultFont === null || trim($defaultFont) === "")) { + $this->defaultFont = $defaultFont; + } else { + $this->defaultFont = "serif"; + } + return $this; + } + + /** + * @return string + */ + public function getDefaultFont() + { + return $this->defaultFont; + } + + /** + * @param string $defaultMediaType + * @return $this + */ + public function setDefaultMediaType($defaultMediaType) + { + $this->defaultMediaType = $defaultMediaType; + return $this; + } + + /** + * @return string + */ + public function getDefaultMediaType() + { + return $this->defaultMediaType; + } + + /** + * @param string|float[] $defaultPaperSize + * @return $this + */ + public function setDefaultPaperSize($defaultPaperSize): self + { + $this->defaultPaperSize = $defaultPaperSize; + return $this; + } + + /** + * @param string $defaultPaperOrientation + * @return $this + */ + public function setDefaultPaperOrientation(string $defaultPaperOrientation): self + { + $this->defaultPaperOrientation = $defaultPaperOrientation; + return $this; + } + + /** + * @return string|float[] + */ + public function getDefaultPaperSize() + { + return $this->defaultPaperSize; + } + + /** + * @return string + */ + public function getDefaultPaperOrientation(): string + { + return $this->defaultPaperOrientation; + } + + /** + * @param int $dpi + * @return $this + */ + public function setDpi($dpi) + { + $this->dpi = $dpi; + return $this; + } + + /** + * @return int + */ + public function getDpi() + { + return $this->dpi; + } + + /** + * @param string $fontCache + * @return $this + */ + public function setFontCache($fontCache) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($fontCache, "fontCache") === true) { + $this->fontCache = $fontCache; + } + return $this; + } + + /** + * @return string + */ + public function getFontCache() + { + return $this->fontCache; + } + + /** + * @param string $fontDir + * @return $this + */ + public function setFontDir($fontDir) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($fontDir, "fontDir") === true) { + $this->fontDir = $fontDir; + } + return $this; + } + + /** + * @return string + */ + public function getFontDir() + { + return $this->fontDir; + } + + /** + * @param float $fontHeightRatio + * @return $this + */ + public function setFontHeightRatio($fontHeightRatio) + { + $this->fontHeightRatio = $fontHeightRatio; + return $this; + } + + /** + * @return float + */ + public function getFontHeightRatio() + { + return $this->fontHeightRatio; + } + + /** + * @param boolean $isFontSubsettingEnabled + * @return $this + */ + public function setIsFontSubsettingEnabled($isFontSubsettingEnabled) + { + $this->isFontSubsettingEnabled = $isFontSubsettingEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsFontSubsettingEnabled() + { + return $this->isFontSubsettingEnabled; + } + + /** + * @return boolean + */ + public function isFontSubsettingEnabled() + { + return $this->getIsFontSubsettingEnabled(); + } + + /** + * @deprecated + * @param boolean $isHtml5ParserEnabled + * @return $this + */ + public function setIsHtml5ParserEnabled($isHtml5ParserEnabled) + { + $this->isHtml5ParserEnabled = $isHtml5ParserEnabled; + return $this; + } + + /** + * @deprecated + * @return boolean + */ + public function getIsHtml5ParserEnabled() + { + return $this->isHtml5ParserEnabled; + } + + /** + * @deprecated + * @return boolean + */ + public function isHtml5ParserEnabled() + { + return $this->getIsHtml5ParserEnabled(); + } + + /** + * @param boolean $isJavascriptEnabled + * @return $this + */ + public function setIsJavascriptEnabled($isJavascriptEnabled) + { + $this->isJavascriptEnabled = $isJavascriptEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsJavascriptEnabled() + { + return $this->isJavascriptEnabled; + } + + /** + * @return boolean + */ + public function isJavascriptEnabled() + { + return $this->getIsJavascriptEnabled(); + } + + /** + * @param boolean $isPhpEnabled + * @return $this + */ + public function setIsPhpEnabled($isPhpEnabled) + { + $this->isPhpEnabled = $isPhpEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsPhpEnabled() + { + return $this->isPhpEnabled; + } + + /** + * @return boolean + */ + public function isPhpEnabled() + { + return $this->getIsPhpEnabled(); + } + + /** + * @param boolean $isRemoteEnabled + * @return $this + */ + public function setIsRemoteEnabled($isRemoteEnabled) + { + $this->isRemoteEnabled = $isRemoteEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsRemoteEnabled() + { + return $this->isRemoteEnabled; + } + + /** + * @return boolean + */ + public function isRemoteEnabled() + { + return $this->getIsRemoteEnabled(); + } + + /** + * @param array|null $allowedRemoteHosts + * @return $this + */ + public function setAllowedRemoteHosts($allowedRemoteHosts) + { + if (is_array($allowedRemoteHosts)) { + // Set hosts to lowercase + foreach ($allowedRemoteHosts as &$host) { + $host = mb_strtolower($host, "UTF-8"); + } + + unset($host); + } + + $this->allowedRemoteHosts = $allowedRemoteHosts; + return $this; + } + + /** + * @return array|null + */ + public function getAllowedRemoteHosts() + { + return $this->allowedRemoteHosts; + } + + /** + * @param boolean $isRemoteEnabled + * @return $this + */ + public function setIsPdfAEnabled($isPdfAEnabled) + { + $this->isPdfAEnabled = $isPdfAEnabled; + return $this; + } + + /** + * @return boolean + */ + public function getIsPdfAEnabled() + { + return $this->isPdfAEnabled; + } + + /** + * @return boolean + */ + public function isPdfAEnabled() + { + return $this->getIsPdfAEnabled(); + } + + /** + * @param string $logOutputFile + * @return $this + */ + public function setLogOutputFile($logOutputFile) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($logOutputFile, "logOutputFile") === true) { + $this->logOutputFile = $logOutputFile; + } + return $this; + } + + /** + * @return string + */ + public function getLogOutputFile() + { + return $this->logOutputFile; + } + + /** + * @param string $tempDir + * @return $this + */ + public function setTempDir($tempDir) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($tempDir, "tempDir") === true) { + $this->tempDir = $tempDir; + } + return $this; + } + + /** + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } + + /** + * @param string $rootDir + * @return $this + */ + public function setRootDir($rootDir) + { + if (!is_callable($this->artifactPathValidation) || ($this->artifactPathValidation)($rootDir, "rootDir") === true) { + $this->rootDir = $rootDir; + } + return $this; + } + + /** + * @return string + */ + public function getRootDir() + { + return $this->rootDir; + } + + /** + * Sets the HTTP context + * + * @param resource|array $httpContext + * @return $this + */ + public function setHttpContext($httpContext) + { + $this->httpContext = is_array($httpContext) ? stream_context_create($httpContext) : $httpContext; + return $this; + } + + /** + * Returns the HTTP context + * + * @return resource + */ + public function getHttpContext() + { + return $this->httpContext; + } + + + public function validateArtifactPath(?string $path, string $option) + { + if ($path === null) { + return true; + } + $parsed_uri = parse_url($path); + if ($parsed_uri === false || (array_key_exists("scheme", $parsed_uri) && strtolower($parsed_uri["scheme"]) === "phar")) { + return false; + } + return true; + } + + public function validateLocalUri(string $uri) + { + if ($uri === null || strlen($uri) === 0) { + return [false, "The URI must not be empty."]; + } + + $realfile = realpath(str_replace("file://", "", $uri)); + + $dirs = $this->chroot; + $dirs[] = $this->rootDir; + $chrootValid = false; + foreach ($dirs as $chrootPath) { + $chrootPath = realpath($chrootPath); + if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) { + $chrootValid = true; + break; + } + } + if ($chrootValid !== true) { + return [false, "Permission denied. The file could not be found under the paths specified by Options::chroot."]; + } + + if (!$realfile) { + return [false, "File not found."]; + } + + return [true, null]; + } + + public function validatePharUri(string $uri) + { + if ($uri === null || strlen($uri) === 0) { + return [false, "The URI must not be empty."]; + } + + $file = substr(substr($uri, 0, strpos($uri, ".phar") + 5), 7); + return $this->validateLocalUri($file); + } + + public function validateRemoteUri(string $uri) + { + if ($uri === null || strlen($uri) === 0) { + return [false, "The URI must not be empty."]; + } + + if (!$this->isRemoteEnabled) { + return [false, "Remote file requested, but remote file download is disabled."]; + } + + if (is_array($this->allowedRemoteHosts) && count($this->allowedRemoteHosts) > 0) { + $host = parse_url($uri, PHP_URL_HOST); + $host = mb_strtolower($host, "UTF-8"); + + if (!in_array($host, $this->allowedRemoteHosts, true)) { + return [false, "Remote host is not in allowed list: " . $host]; + } + } + + return [true, null]; + } +} diff --git a/application/third_party/dompdf-3.1.4/src/PhpEvaluator.php b/application/third_party/dompdf-3.1.4/src/PhpEvaluator.php new file mode 100644 index 0000000..4a46555 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/PhpEvaluator.php @@ -0,0 +1,62 @@ +_canvas = $canvas; + } + + /** + * @param $code + * @param array $vars + */ + public function evaluate($code, $vars = []) + { + if (!$this->_canvas->get_dompdf()->getOptions()->getIsPhpEnabled()) { + return; + } + + // Set up some variables for the inline code + $pdf = $this->_canvas; + $fontMetrics = $pdf->get_dompdf()->getFontMetrics(); + $PAGE_NUM = $pdf->get_page_number(); + $PAGE_COUNT = $pdf->get_page_count(); + + // Override those variables if passed in + foreach ($vars as $k => $v) { + $$k = $v; + } + + eval($code); + } + + /** + * @param Frame $frame + */ + public function render(Frame $frame) + { + $this->evaluate($frame->get_node()->nodeValue); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/Absolute.php b/application/third_party/dompdf-3.1.4/src/Positioner/Absolute.php new file mode 100644 index 0000000..2df9a74 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/Absolute.php @@ -0,0 +1,128 @@ +get_reflower() instanceof Block) { + $style = $frame->get_style(); + [$cbx, $cby, $cbw, $cbh] = $frame->get_containing_block(); + + // If the `top` value is `auto`, the frame will be repositioned + // after its height has been resolved + $left = (float) $style->length_in_pt($style->left, $cbw); + $top = (float) $style->length_in_pt($style->top, $cbh); + + $frame->set_position($cbx + $left, $cby + $top); + } else { + // Legacy positioning logic for image and table frames + // TODO: Resolve dimensions, margins, and offsets similar to the + // block case in the reflowers and use the simplified logic above + $style = $frame->get_style(); + $block_parent = $frame->find_block_parent(); + $current_line = $block_parent->get_current_line_box(); + + list($x, $y, $w, $h) = $frame->get_containing_block(); + $inflow_x = $block_parent->get_content_box()["x"] + $current_line->left + $current_line->w; + $inflow_y = $current_line->y; + + $top = $style->length_in_pt($style->top, $h); + $right = $style->length_in_pt($style->right, $w); + $bottom = $style->length_in_pt($style->bottom, $h); + $left = $style->length_in_pt($style->left, $w); + + list($width, $height) = [$frame->get_margin_width(), $frame->get_margin_height()]; + + $orig_width = $style->get_specified("width"); + $orig_height = $style->get_specified("height"); + + /**************************** + * + * Width auto: + * ____________| left=auto | left=fixed | + * right=auto | A | B | + * right=fixed | C | D | + * + * Width fixed: + * ____________| left=auto | left=fixed | + * right=auto | E | F | + * right=fixed | G | H | + *****************************/ + + if ($left === "auto") { + if ($right === "auto") { + // A or E - Keep the frame at the same position + $x = $inflow_x; + } else { + if ($orig_width === "auto") { + // C + $x += $w - $width - $right; + } else { + // G + $x += $w - $width - $right; + } + } + } else { + if ($right === "auto") { + // B or F + $x += (float)$left; + } else { + if ($orig_width === "auto") { + // D - TODO change width + $x += (float)$left; + } else { + // H - Everything is fixed: left + width win + $x += (float)$left; + } + } + } + + // The same vertically + if ($top === "auto") { + if ($bottom === "auto") { + // A or E - Keep the frame at the same position + $y = $inflow_y; + } else { + if ($orig_height === "auto") { + // C + $y += (float)$h - $height - (float)$bottom; + } else { + // G + $y += (float)$h - $height - (float)$bottom; + } + } + } else { + if ($bottom === "auto") { + // B or F + $y += (float)$top; + } else { + if ($orig_height === "auto") { + // D - TODO change height + $y += (float)$top; + } else { + // H - Everything is fixed: top + height win + $y += (float)$top; + } + } + } + + $frame->set_position($x, $y); + } + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/AbstractPositioner.php b/application/third_party/dompdf-3.1.4/src/Positioner/AbstractPositioner.php new file mode 100644 index 0000000..a75c09f --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/AbstractPositioner.php @@ -0,0 +1,48 @@ +get_position(); + + if (!$ignore_self) { + $frame->set_position($x + $offset_x, $y + $offset_y); + } + + foreach ($frame->get_children() as $child) { + $child->move($offset_x, $offset_y); + } + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/Block.php b/application/third_party/dompdf-3.1.4/src/Positioner/Block.php new file mode 100644 index 0000000..e6f65ea --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/Block.php @@ -0,0 +1,40 @@ +get_style(); + $cb = $frame->get_containing_block(); + $p = $frame->find_block_parent(); + + if ($p) { + $float = $style->float; + + if (!$float || $float === "none") { + $p->add_line(true); + } + $y = $p->get_current_line_box()->y; + } else { + $y = $cb["y"]; + } + + $x = $cb["x"]; + + $frame->set_position($x, $y); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/Fixed.php b/application/third_party/dompdf-3.1.4/src/Positioner/Fixed.php new file mode 100644 index 0000000..13eb9e9 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/Fixed.php @@ -0,0 +1,92 @@ +get_reflower() instanceof Block) { + parent::position($frame); + } else { + // Legacy positioning logic for image and table frames + // TODO: Resolve dimensions, margins, and offsets similar to the + // block case in the reflowers and use the simplified logic above + $style = $frame->get_style(); + $root = $frame->get_root(); + $initialcb = $root->get_containing_block(); + $initialcb_style = $root->get_style(); + + $p = $frame->find_block_parent(); + if ($p) { + $p->add_line(); + } + // Compute the margins of the @page style + $margin_top = (float)$initialcb_style->length_in_pt($initialcb_style->margin_top, $initialcb["h"]); + $margin_right = (float)$initialcb_style->length_in_pt($initialcb_style->margin_right, $initialcb["w"]); + $margin_bottom = (float)$initialcb_style->length_in_pt($initialcb_style->margin_bottom, $initialcb["h"]); + $margin_left = (float)$initialcb_style->length_in_pt($initialcb_style->margin_left, $initialcb["w"]); + + // The needed computed style of the element + $height = (float)$style->length_in_pt($style->get_specified("height"), $initialcb["h"]); + $width = (float)$style->length_in_pt($style->get_specified("width"), $initialcb["w"]); + + $top = $style->length_in_pt($style->get_specified("top"), $initialcb["h"]); + $right = $style->length_in_pt($style->get_specified("right"), $initialcb["w"]); + $bottom = $style->length_in_pt($style->get_specified("bottom"), $initialcb["h"]); + $left = $style->length_in_pt($style->get_specified("left"), $initialcb["w"]); + + $y = $margin_top; + if (isset($top)) { + $y = (float)$top + $margin_top; + if ($top === "auto") { + $y = $margin_top; + if (isset($bottom) && $bottom !== "auto") { + $y = $initialcb["h"] - $bottom - $margin_bottom; + if ($frame->is_auto_height()) { + $y -= $height; + } else { + $y -= $frame->get_margin_height(); + } + } + } + } + + $x = $margin_left; + if (isset($left)) { + $x = (float)$left + $margin_left; + if ($left === "auto") { + $x = $margin_left; + if (isset($right) && $right !== "auto") { + $x = $initialcb["w"] - $right - $margin_right; + if ($frame->is_auto_width()) { + $x -= $width; + } else { + $x -= $frame->get_margin_width(); + } + } + } + } + + $frame->set_position($x, $y); + + foreach ($frame->get_children() as $child) { + $child->set_position($x, $y); + } + } + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/Inline.php b/application/third_party/dompdf-3.1.4/src/Positioner/Inline.php new file mode 100644 index 0000000..f1036e9 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/Inline.php @@ -0,0 +1,56 @@ +find_block_parent(); + $cb = $frame->get_containing_block(); + + if (!$block) { + // FIXME: An inline frame without block parent should not be + // possible, but this can occur currently when the body is styled + // with `display: inline !important;` or `display: inline-block !important;` + $frame->set_position($cb["x"], $cb["y"]); + return; + } + + $line = $block->get_current_line_box(); + + if (!$frame->is_text_node() && !($frame instanceof InlineFrameDecorator)) { + // Atomic inline boxes and replaced inline elements + // (inline-block, inline-table, img etc.) + $width = $frame->get_margin_width(); + $available_width = $cb["w"] - $line->left - $line->w - $line->right; + + if (Helpers::lengthGreater($width, $available_width)) { + $block->add_line(); + $line = $block->get_current_line_box(); + } + } + + $frame->set_position($cb["x"] + $line->w, $line->y); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/ListBullet.php b/application/third_party/dompdf-3.1.4/src/Positioner/ListBullet.php new file mode 100644 index 0000000..081d594 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/ListBullet.php @@ -0,0 +1,42 @@ +get_parent(); + $style = $parent->get_style(); + $cbw = $parent->get_containing_block("w"); + $margin_left = (float) $style->length_in_pt($style->margin_left, $cbw); + $border_edge = $parent->get_position("x") + $margin_left; + + // This includes the marker indentation + $x = $border_edge - $frame->get_margin_width(); + + // The marker is later vertically aligned with the corresponding line + // box and its vertical position is fine-tuned in the renderer + $p = $frame->find_block_parent(); + $y = $p->get_current_line_box()->y; + + $frame->set_position($x, $y); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/NullPositioner.php b/application/third_party/dompdf-3.1.4/src/Positioner/NullPositioner.php new file mode 100644 index 0000000..6ad425c --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/NullPositioner.php @@ -0,0 +1,26 @@ +get_cellmap(); + $frame->set_position($cellmap->get_frame_position($frame)); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Positioner/TableRow.php b/application/third_party/dompdf-3.1.4/src/Positioner/TableRow.php new file mode 100644 index 0000000..79c0fcf --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Positioner/TableRow.php @@ -0,0 +1,34 @@ +get_containing_block(); + $p = $frame->get_prev_sibling(); + + if ($p) { + $y = $p->get_position("y") + $p->get_margin_height(); + } else { + $y = $cb["y"]; + } + $frame->set_position($cb["x"], $y); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer.php b/application/third_party/dompdf-3.1.4/src/Renderer.php new file mode 100644 index 0000000..15391fb --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer.php @@ -0,0 +1,307 @@ +_canvas->new_page(); + } + + /** + * Render frames recursively + * + * @param Frame $frame the frame to render + */ + public function render(Frame $frame) + { + global $_dompdf_debug; + + $this->_check_callbacks("begin_frame", $frame); + + if ($_dompdf_debug) { + echo $frame; + flush(); + } + + $style = $frame->get_style(); + + if (in_array($style->visibility, ["hidden", "collapse"], true)) { + return; + } + + $display = $style->display; + $transformList = $style->transform; + $hasTransform = $transformList !== []; + + // Starts the CSS transformation + if ($hasTransform) { + $this->_canvas->save(); + + [$x, $y] = $frame->get_padding_box(); + [$originX, $originY] = $style->transform_origin; + $w = (float) $style->length_in_pt($style->width); + $h = (float) $style->length_in_pt($style->height); + + foreach ($transformList as $transform) { + [$function, $values] = $transform; + + if ($function === "matrix") { + $function = "transform"; + } elseif ($function === "translate") { + $values[0] = $style->length_in_pt($values[0], $w); + $values[1] = $style->length_in_pt($values[1], $h); + } + + $values[] = $x + $style->length_in_pt($originX, $w); + $values[] = $y + $style->length_in_pt($originY, $h); + + call_user_func_array([$this->_canvas, $function], $values); + } + } + + switch ($display) { + + case "block": + case "list-item": + case "inline-block": + case "table": + case "inline-table": + $this->_render_frame("block", $frame); + break; + + case "inline": + if ($frame->is_text_node()) { + $this->_render_frame("text", $frame); + } else { + $this->_render_frame("inline", $frame); + } + break; + + case "table-cell": + $this->_render_frame("table-cell", $frame); + break; + + case "table-row": + $this->_render_frame("table-row", $frame); + break; + + case "table-row-group": + case "table-header-group": + case "table-footer-group": + $this->_render_frame("table-row-group", $frame); + break; + + case "-dompdf-list-bullet": + $this->_render_frame("list-bullet", $frame); + break; + + case "-dompdf-image": + $this->_render_frame("image", $frame); + break; + + case "none": + $node = $frame->get_node(); + + if ($node->nodeName === "script") { + if ($node->getAttribute("type") === "text/php" || + $node->getAttribute("language") === "php" + ) { + // Evaluate embedded php scripts + $this->_render_frame("php", $frame); + } elseif ($node->getAttribute("type") === "text/javascript" || + $node->getAttribute("language") === "javascript" + ) { + // Insert JavaScript + $this->_render_frame("javascript", $frame); + } + } + + // Don't render children, so skip to next iter + return; + + default: + break; + + } + + // Starts the overflow: hidden box + if ($style->overflow === "hidden") { + $padding_box = $frame->get_padding_box(); + [$x, $y, $w, $h] = $padding_box; + $style = $frame->get_style(); + + if ($style->has_border_radius()) { + $border_box = $frame->get_border_box(); + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $padding_box); + $this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl); + } else { + $this->_canvas->clipping_rectangle($x, $y, $w, $h); + } + } + + $stack = []; + + foreach ($frame->get_children() as $child) { + // < 0 : negative z-index + // = 0 : no z-index, no stacking context + // = 1 : stacking context without z-index + // > 1 : z-index + $child_style = $child->get_style(); + $child_z_index = $child_style->z_index; + $z_index = 0; + + if ($child_z_index !== "auto") { + $z_index = $child_z_index + 1; + } elseif ($child_style->float !== "none" || $child->is_positioned()) { + $z_index = 1; + } + + $stack[$z_index][] = $child; + } + + ksort($stack); + + foreach ($stack as $by_index) { + foreach ($by_index as $child) { + $this->render($child); + } + } + + // Ends the overflow: hidden box + if ($style->overflow === "hidden") { + $this->_canvas->clipping_end(); + } + + if ($hasTransform) { + $this->_canvas->restore(); + } + + // Check for end frame callback + $this->_check_callbacks("end_frame", $frame); + } + + /** + * Check for callbacks that need to be performed when a given event + * gets triggered on a frame + * + * @param string $event The type of event + * @param Frame $frame The frame that event is triggered on + */ + protected function _check_callbacks(string $event, Frame $frame): void + { + if (!isset($this->_callbacks)) { + $this->_callbacks = $this->_dompdf->getCallbacks(); + } + + if (isset($this->_callbacks[$event])) { + $fs = $this->_callbacks[$event]; + $canvas = $this->_canvas; + $fontMetrics = $this->_dompdf->getFontMetrics(); + + foreach ($fs as $f) { + $f($frame, $canvas, $fontMetrics); + } + } + } + + /** + * Render a single frame + * + * Creates Renderer objects on demand + * + * @param string $type type of renderer to use + * @param Frame $frame the frame to render + */ + protected function _render_frame($type, $frame) + { + + if (!isset($this->_renderers[$type])) { + + switch ($type) { + case "block": + $this->_renderers[$type] = new Block($this->_dompdf); + break; + + case "inline": + $this->_renderers[$type] = new Inline($this->_dompdf); + break; + + case "text": + $this->_renderers[$type] = new Text($this->_dompdf); + break; + + case "image": + $this->_renderers[$type] = new Image($this->_dompdf); + break; + + case "table-cell": + $this->_renderers[$type] = new TableCell($this->_dompdf); + break; + + case "table-row": + $this->_renderers[$type] = new TableRow($this->_dompdf); + break; + + case "table-row-group": + $this->_renderers[$type] = new TableRowGroup($this->_dompdf); + break; + + case "list-bullet": + $this->_renderers[$type] = new ListBullet($this->_dompdf); + break; + + case "php": + $this->_renderers[$type] = new PhpEvaluator($this->_canvas); + break; + + case "javascript": + $this->_renderers[$type] = new JavascriptEmbedder($this->_dompdf); + break; + + } + } + + $this->_renderers[$type]->render($frame); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/AbstractRenderer.php b/application/third_party/dompdf-3.1.4/src/Renderer/AbstractRenderer.php new file mode 100644 index 0000000..293bed2 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/AbstractRenderer.php @@ -0,0 +1,1298 @@ +_dompdf = $dompdf; + $this->_canvas = $dompdf->getCanvas(); + } + + /** + * Render a frame. + * + * Specialized in child classes + * + * @param Frame $frame The frame to render + */ + abstract function render(Frame $frame); + + /** + * @param Frame $frame + * @param float[] $border_box + */ + protected function _render_background(Frame $frame, array $border_box): void + { + $style = $frame->get_style(); + $color = $style->background_color; + $image = $style->background_image; + [$x, $y, $w, $h] = $border_box; + + if ($color === "transparent" && $image === "none") { + return; + } + + if ($style->has_border_radius()) { + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box); + $this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl); + } + + if ($color !== "transparent") { + $this->_canvas->filled_rectangle($x, $y, $w, $h, $color); + } + + if ($image !== "none") { + $this->_background_image($image, $x, $y, $w, $h, $style); + } + + if ($style->has_border_radius()) { + $this->_canvas->clipping_end(); + } + } + + /** + * @param Frame $frame + * @param float[] $border_box + * @param string $corner_style + */ + protected function _render_border(Frame $frame, array $border_box, string $corner_style = "bevel"): void + { + $style = $frame->get_style(); + $bp = $style->get_border_properties(); + [$x, $y, $w, $h] = $border_box; + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box); + + // Short-cut: If all the borders are "solid" with the same color and + // style, and no radius, we'd better draw a rectangle + if ($bp["top"]["style"] === "solid" && + $bp["top"] === $bp["right"] && + $bp["right"] === $bp["bottom"] && + $bp["bottom"] === $bp["left"] && + !$style->has_border_radius() + ) { + $props = $bp["top"]; + if ($props["color"] === "transparent" || $props["width"] <= 0) { + return; + } + + $width = (float)$style->length_in_pt($props["width"]); + $this->_canvas->rectangle($x + $width / 2, $y + $width / 2, $w - $width, $h - $width, $props["color"], $width); + return; + } + + // Do it the long way + $widths = [ + (float)$style->length_in_pt($bp["top"]["width"]), + (float)$style->length_in_pt($bp["right"]["width"]), + (float)$style->length_in_pt($bp["bottom"]["width"]), + (float)$style->length_in_pt($bp["left"]["width"]) + ]; + + foreach ($bp as $side => $props) { + if ($props["style"] === "none" || + $props["style"] === "hidden" || + $props["color"] === "transparent" || + $props["width"] <= 0 + ) { + continue; + } + + [$x, $y, $w, $h] = $border_box; + $method = "_border_" . $props["style"]; + + switch ($side) { + case "top": + $length = $w; + $r1 = $tl; + $r2 = $tr; + break; + + case "bottom": + $length = $w; + $y += $h; + $r1 = $bl; + $r2 = $br; + break; + + case "left": + $length = $h; + $r1 = $tl; + $r2 = $bl; + break; + + case "right": + $length = $h; + $x += $w; + $r1 = $tr; + $r2 = $br; + break; + + default: + break; + } + + // draw rounded corners + $this->$method($x, $y, $length, $props["color"], $widths, $side, $corner_style, $r1, $r2); + } + } + + /** + * @param Frame $frame + * @param float[] $border_box + * @param string $corner_style + */ + protected function _render_outline(Frame $frame, array $border_box, string $corner_style = "bevel"): void + { + $style = $frame->get_style(); + + $width = $style->outline_width; + $outline_style = $style->outline_style; + $color = $style->outline_color; + + if ($outline_style === "none" || $color === "transparent" || $width <= 0) { + return; + } + + $offset = $style->outline_offset; + + [$x, $y, $w, $h] = $border_box; + $d = $width + $offset; + $outline_box = [$x - $d, $y - $d, $w + $d * 2, $h + $d * 2]; + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $outline_box); + + $x -= $offset; + $y -= $offset; + $w += $offset * 2; + $h += $offset * 2; + + // For a simple outline, we can draw a rectangle + if ($outline_style === "solid" && !$style->has_border_radius()) { + $x -= $width / 2; + $y -= $width / 2; + $w += $width; + $h += $width; + + $this->_canvas->rectangle($x, $y, $w, $h, $color, $width); + return; + } + + $x -= $width; + $y -= $width; + $w += $width * 2; + $h += $width * 2; + + $method = "_border_" . $outline_style; + $widths = array_fill(0, 4, $width); + $sides = ["top", "right", "left", "bottom"]; + + foreach ($sides as $side) { + switch ($side) { + case "top": + $length = $w; + $side_x = $x; + $side_y = $y; + $r1 = $tl; + $r2 = $tr; + break; + + case "bottom": + $length = $w; + $side_x = $x; + $side_y = $y + $h; + $r1 = $bl; + $r2 = $br; + break; + + case "left": + $length = $h; + $side_x = $x; + $side_y = $y; + $r1 = $tl; + $r2 = $bl; + break; + + case "right": + $length = $h; + $side_x = $x + $w; + $side_y = $y; + $r1 = $tr; + $r2 = $br; + break; + + default: + break; + } + + $this->$method($side_x, $side_y, $length, $color, $widths, $side, $corner_style, $r1, $r2); + } + } + + /** + * Render a background image over a rectangular area + * + * @param string $url The background image to load + * @param float $x The left edge of the rectangular area + * @param float $y The top edge of the rectangular area + * @param float $width The width of the rectangular area + * @param float $height The height of the rectangular area + * @param Style $style The associated Style object + * + * @throws \Exception + */ + protected function _background_image(string $url, float $x, float $y, float $width, float $height, Style $style): void + { + if (!function_exists("imagecreatetruecolor")) { + throw new \Exception("The PHP GD extension is required, but is not installed."); + } + + $sheet = $style->get_stylesheet(); + + // Skip degenerate cases + if ($width == 0 || $height == 0) { + return; + } + + $box_width = $width; + $box_height = $height; + + //debugpng + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[_background_image ' . $url . ']'; + } + + list($img, $type, /*$msg*/) = Cache::resolve_url( + $url, + $sheet->get_protocol(), + $sheet->get_host(), + $sheet->get_base_path(), + $this->_dompdf->getOptions() + ); + + // Bail if the image is no good + if (Cache::is_broken($img)) { + return; + } + + //Try to optimize away reading and composing of same background multiple times + //Postponing read with imagecreatefrom ...() + //final composition parameters and name not known yet + //Therefore read dimension directly from file, instead of creating gd object first. + //$img_w = imagesx($src); $img_h = imagesy($src); + + list($img_w, $img_h) = Helpers::dompdf_getimagesize($img, $this->_dompdf->getHttpContext()); + if ($img_w == 0 || $img_h == 0) { + return; + } + + // save for later check if file needs to be resized. + $org_img_w = $img_w; + $org_img_h = $img_h; + + $repeat = $style->background_repeat; + $dpi = $this->_dompdf->getOptions()->getDpi(); + + //Increase background resolution and dependent box size according to image resolution to be placed in + //Then image can be copied in without resize + $bg_width = round((float)($width * $dpi) / 72); + $bg_height = round((float)($height * $dpi) / 72); + + list($img_w, $img_h) = $this->_resize_background_image( + $img_w, + $img_h, + $bg_width, + $bg_height, + $style->background_size, + $dpi + ); + //Need %bg_x, $bg_y as background pos, where img starts, converted to pixel + + list($bg_x, $bg_y) = $style->background_position; + + if (Helpers::is_percent($bg_x)) { + // The point $bg_x % from the left edge of the image is placed + // $bg_x % from the left edge of the background rectangle + $p = ((float)$bg_x) / 100.0; + $x1 = $p * $img_w; + $x2 = $p * $bg_width; + + $bg_x = $x2 - $x1; + } else { + $bg_x = (float)($style->length_in_pt($bg_x) * $dpi) / 72; + } + + $bg_x = round($bg_x + (float)$style->length_in_pt($style->border_left_width) * $dpi / 72); + + if (Helpers::is_percent($bg_y)) { + // The point $bg_y % from the left edge of the image is placed + // $bg_y % from the left edge of the background rectangle + $p = ((float)$bg_y) / 100.0; + $y1 = $p * $img_h; + $y2 = $p * $bg_height; + + $bg_y = $y2 - $y1; + } else { + $bg_y = (float)($style->length_in_pt($bg_y) * $dpi) / 72; + } + + $bg_y = round($bg_y + (float)$style->length_in_pt($style->border_top_width) * $dpi / 72); + + //clip background to the image area on partial repeat. Nothing to do if img off area + //On repeat, normalize start position to the tile at immediate left/top or 0/0 of area + //On no repeat with positive offset: move size/start to have offset==0 + //Handle x/y Dimensions separately + + if ($repeat !== "repeat" && $repeat !== "repeat-x") { + //No repeat x + if ($bg_x < 0) { + $bg_width = $img_w + $bg_x; + } else { + $x += ($bg_x * 72) / $dpi; + $bg_width = $bg_width - $bg_x; + if ($bg_width > $img_w) { + $bg_width = $img_w; + } + $bg_x = 0; + } + + if ($bg_width <= 0) { + return; + } + + $width = (float)($bg_width * 72) / $dpi; + } else { + //repeat x + if ($bg_x < 0) { + $bg_x = -((-$bg_x) % $img_w); + } else { + $bg_x = $bg_x % $img_w; + if ($bg_x > 0) { + $bg_x -= $img_w; + } + } + } + + if ($repeat !== "repeat" && $repeat !== "repeat-y") { + //no repeat y + if ($bg_y < 0) { + $bg_height = $img_h + $bg_y; + } else { + $y += ($bg_y * 72) / $dpi; + $bg_height = $bg_height - $bg_y; + if ($bg_height > $img_h) { + $bg_height = $img_h; + } + $bg_y = 0; + } + if ($bg_height <= 0) { + return; + } + $height = (float)($bg_height * 72) / $dpi; + } else { + //repeat y + if ($bg_y < 0) { + $bg_y = -((-$bg_y) % $img_h); + } else { + $bg_y = $bg_y % $img_h; + if ($bg_y > 0) { + $bg_y -= $img_h; + } + } + } + + //Optimization, if repeat has no effect + if ($repeat === "repeat" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) { + $repeat = "repeat-x"; + } + + if ($repeat === "repeat" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) { + $repeat = "repeat-y"; + } + + if (($repeat === "repeat-x" && $bg_x <= 0 && $img_w + $bg_x >= $bg_width) || + ($repeat === "repeat-y" && $bg_y <= 0 && $img_h + $bg_y >= $bg_height) + ) { + $repeat = "no-repeat"; + } + + // Avoid rendering identical background-image variants multiple times + // This is not dependent of background color of box! .'_'.(is_array($bg_color) ? $bg_color["hex"] : $bg_color) + // Note: Here, bg_* are the start values, not end values after going through the tile loops! + + $key = implode("_", [$bg_width, $bg_height, $img_w, $img_h, $bg_x, $bg_y, $repeat]); + // FIXME: This will fail when a file with that exact name exists in the + // same directory, included in the document as regular image + $cpdfKey = $img . "_" . $key; + $tmpFile = Cache::getTempImage($img, $key); + $cached = ($this->_canvas instanceof CPDF && $this->_canvas->get_cpdf()->image_iscached($cpdfKey)) + || ($tmpFile !== null && file_exists($tmpFile)); + + if (!$cached) { + // img: image url string + // img_w, img_h: original image size in px + // width, height: box size in pt + // bg_width, bg_height: box size in px + // x, y: left/top edge of box on page in pt + // start_x, start_y: placement of image relative to pattern + // $repeat: repeat mode + // $bg: GD object of result image + // $src: GD object of original image + + // Create a new image to fit over the background rectangle + $bg = imagecreatetruecolor($bg_width, $bg_height); + $cpdfFromGd = true; + + switch (strtolower($type)) { + case "png": + $cpdfFromGd = false; + imagesavealpha($bg, true); + imagealphablending($bg, false); + $src = @imagecreatefrompng($img); + break; + + case "jpeg": + $src = @imagecreatefromjpeg($img); + break; + + case "webp": + $src = @imagecreatefromwebp($img); + break; + + case "gif": + $src = @imagecreatefromgif($img); + break; + + case "bmp": + $src = @Helpers::imagecreatefrombmp($img); + break; + + default: + return; // Unsupported image type + } + + if ($src == null) { + return; + } + + if ($img_w != $org_img_w || $img_h != $org_img_h) { + $newSrc = imagecreatetruecolor($img_w, $img_h); + imagealphablending($newSrc, false); + imagesavealpha($newSrc, true); + imagecopyresampled($newSrc, $src, 0, 0, 0, 0, $img_w, $img_h, imagesx($src), imagesy($src)); + if (PHP_MAJOR_VERSION < 8) { + imagedestroy($src); + } + $src = $newSrc; + } + + if ($src == null) { + return; + } + + //Background color if box is not relevant here + //Non transparent image: box clipped to real size. Background non relevant. + //Transparent image: The image controls the transparency and lets shine through whatever background. + //However on transparent image preset the composed image with the transparency color, + //to keep the transparency when copying over the non transparent parts of the tiles. + $ti = imagecolortransparent($src); + $palletsize = imagecolorstotal($src); + + if ($ti >= 0 && $ti < $palletsize) { + $tc = imagecolorsforindex($src, $ti); + $ti = imagecolorallocate($bg, $tc['red'], $tc['green'], $tc['blue']); + imagefill($bg, 0, 0, $ti); + imagecolortransparent($bg, $ti); + } + + //This has only an effect for the non repeatable dimension. + //compute start of src and dest coordinates of the single copy + if ($bg_x < 0) { + $dst_x = 0; + $src_x = -$bg_x; + } else { + $src_x = 0; + $dst_x = $bg_x; + } + + if ($bg_y < 0) { + $dst_y = 0; + $src_y = -$bg_y; + } else { + $src_y = 0; + $dst_y = $bg_y; + } + + //For historical reasons exchange meanings of variables: + //start_* will be the start values, while bg_* will be the temporary start values in the loops + $start_x = $bg_x; + $start_y = $bg_y; + + // Copy regions from the source image to the background + if ($repeat === "no-repeat") { + // Simply place the image on the background + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $img_h); + + } elseif ($repeat === "repeat-x") { + for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) { + if ($bg_x < 0) { + $dst_x = 0; + $src_x = -$bg_x; + $w = $img_w + $bg_x; + } else { + $dst_x = $bg_x; + $src_x = 0; + $w = $img_w; + } + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $img_h); + } + } elseif ($repeat === "repeat-y") { + + for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) { + if ($bg_y < 0) { + $dst_y = 0; + $src_y = -$bg_y; + $h = $img_h + $bg_y; + } else { + $dst_y = $bg_y; + $src_y = 0; + $h = $img_h; + } + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $img_w, $h); + } + } elseif ($repeat === "repeat") { + for ($bg_y = $start_y; $bg_y < $bg_height; $bg_y += $img_h) { + for ($bg_x = $start_x; $bg_x < $bg_width; $bg_x += $img_w) { + if ($bg_x < 0) { + $dst_x = 0; + $src_x = -$bg_x; + $w = $img_w + $bg_x; + } else { + $dst_x = $bg_x; + $src_x = 0; + $w = $img_w; + } + + if ($bg_y < 0) { + $dst_y = 0; + $src_y = -$bg_y; + $h = $img_h + $bg_y; + } else { + $dst_y = $bg_y; + $src_y = 0; + $h = $img_h; + } + imagecopy($bg, $src, $dst_x, $dst_y, $src_x, $src_y, $w, $h); + } + } + } else { + print 'Unknown repeat!'; + } + + if (PHP_MAJOR_VERSION < 8) { + imagedestroy($src); + } + + if ($cpdfFromGd && $this->_canvas instanceof CPDF) { + // Skip writing temp file as the GD object is added directly + } else { + $tmpDir = $this->_dompdf->getOptions()->getTempDir(); + $tmpName = @tempnam($tmpDir, "bg_dompdf_img_"); + @unlink($tmpName); + $tmpFile = "$tmpName.png"; + + imagepng($bg, $tmpFile); + if (PHP_MAJOR_VERSION < 8) { + imagedestroy($bg); + } + + Cache::addTempImage($img, $tmpFile, $key); + } + } else { + $bg = null; + $cpdfFromGd = $tmpFile === null; + } + + if ($this->_dompdf->getOptions()->getDebugPng()) { + print '[_background_image ' . $tmpFile . ']'; + } + + $this->_canvas->clipping_rectangle($x, $y, $box_width, $box_height); + + // When using cpdf and optimization to direct png creation from gd object is available, + // don't create temp file, but place gd object directly into the pdf + if ($cpdfFromGd && $this->_canvas instanceof CPDF) { + // Note: CPDF_Adapter image converts y position + $this->_canvas->get_cpdf()->addImagePng($bg, $cpdfKey, $x, $this->_canvas->get_height() - $y - $height, $width, $height); + + if (isset($bg) && PHP_MAJOR_VERSION < 8) { + imagedestroy($bg); + } + } else { + $this->_canvas->image($tmpFile, $x, $y, $width, $height); + } + + $this->_canvas->clipping_end(); + } + + /** + * @param float $img_width + * @param float $img_height + * @param float $container_width + * @param float $container_height + * @param array|string $bg_resize + * @param int $dpi + * + * @return float[] + */ + protected function _resize_background_image( + float $img_width, + float $img_height, + float $container_width, + float $container_height, + $bg_resize, + int $dpi + ): array { + // We got two some specific numbers and/or auto definitions + if (is_array($bg_resize)) { + $is_auto_width = $bg_resize[0] === 'auto'; + if ($is_auto_width) { + $new_img_width = $img_width; + } else { + $new_img_width = $bg_resize[0]; + if (Helpers::is_percent($new_img_width)) { + $new_img_width = round(($container_width / 100) * (float)$new_img_width); + } else { + $new_img_width = round($new_img_width * $dpi / 72); + } + } + + $is_auto_height = $bg_resize[1] === 'auto'; + if ($is_auto_height) { + $new_img_height = $img_height; + } else { + $new_img_height = $bg_resize[1]; + if (Helpers::is_percent($new_img_height)) { + $new_img_height = round(($container_height / 100) * (float)$new_img_height); + } else { + $new_img_height = round($new_img_height * $dpi / 72); + } + } + + // if one of both was set to auto the other one needs to scale proportionally + if ($is_auto_width !== $is_auto_height) { + if ($is_auto_height) { + $new_img_height = round($new_img_width * ($img_height / $img_width)); + } else { + $new_img_width = round($new_img_height * ($img_width / $img_height)); + } + } + } else { + $container_ratio = $container_height / $container_width; + + if ($bg_resize === 'cover' || $bg_resize === 'contain') { + $img_ratio = $img_height / $img_width; + + if ( + ($bg_resize === 'cover' && $container_ratio > $img_ratio) || + ($bg_resize === 'contain' && $container_ratio < $img_ratio) + ) { + $new_img_height = $container_height; + $new_img_width = round($container_height / $img_ratio); + } else { + $new_img_width = $container_width; + $new_img_height = round($container_width * $img_ratio); + } + } else { + $new_img_width = $img_width; + $new_img_height = $img_height; + } + } + + return [$new_img_width, $new_img_height]; + } + + // Border rendering functions + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_dotted($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + $this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dotted", $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_dashed($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + $this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "dashed", $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_solid($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + $this->_border_line($x, $y, $length, $color, $widths, $side, $corner_style, "solid", $r1, $r2); + } + + /** + * @param string $side + * @param float $ratio + * @param float $top + * @param float $right + * @param float $bottom + * @param float $left + * @param float $x + * @param float $y + * @param float $length + * @param float $r1 + * @param float $r2 + */ + protected function _apply_ratio($side, $ratio, $top, $right, $bottom, $left, &$x, &$y, &$length, &$r1, &$r2) + { + switch ($side) { + case "top": + $r1 -= $left * $ratio; + $r2 -= $right * $ratio; + $x += $left * $ratio; + $y += $top * $ratio; + $length -= $left * $ratio + $right * $ratio; + break; + + case "bottom": + $r1 -= $right * $ratio; + $r2 -= $left * $ratio; + $x += $left * $ratio; + $y -= $bottom * $ratio; + $length -= $left * $ratio + $right * $ratio; + break; + + case "left": + $r1 -= $top * $ratio; + $r2 -= $bottom * $ratio; + $x += $left * $ratio; + $y += $top * $ratio; + $length -= $top * $ratio + $bottom * $ratio; + break; + + case "right": + $r1 -= $bottom * $ratio; + $r2 -= $top * $ratio; + $x -= $right * $ratio; + $y += $top * $ratio; + $length -= $top * $ratio + $bottom * $ratio; + break; + + default: + return; + } + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_double($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + list($top, $right, $bottom, $left) = $widths; + + $third_widths = [$top / 3, $right / 3, $bottom / 3, $left / 3]; + + // draw the outer border + $this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2); + + $this->_apply_ratio($side, 2 / 3, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2); + + $this->_border_solid($x, $y, $length, $color, $third_widths, $side, $corner_style, $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_groove($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + list($top, $right, $bottom, $left) = $widths; + + $half_widths = [$top / 2, $right / 2, $bottom / 2, $left / 2]; + + $this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + + $this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2); + + $this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_ridge($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + list($top, $right, $bottom, $left) = $widths; + + $half_widths = [$top / 2, $right / 2, $bottom / 2, $left / 2]; + + $this->_border_outset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + + $this->_apply_ratio($side, 0.5, $top, $right, $bottom, $left, $x, $y, $length, $r1, $r2); + + $this->_border_inset($x, $y, $length, $color, $half_widths, $side, $corner_style, $r1, $r2); + } + + /** + * @param $c + * @return mixed + */ + protected function _tint($c) + { + if (!is_numeric($c)) { + return $c; + } + + return min(1, $c + 0.16); + } + + /** + * @param $c + * @return mixed + */ + protected function _shade($c) + { + if (!is_numeric($c)) { + return $c; + } + + return max(0, $c - 0.33); + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_inset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + switch ($side) { + case "top": + case "left": + $shade = array_map([$this, "_shade"], $color); + $this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2); + break; + + case "bottom": + case "right": + $tint = array_map([$this, "_tint"], $color); + $this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2); + break; + + default: + return; + } + } + + /** + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param float $r1 + * @param float $r2 + */ + protected function _border_outset($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $r1 = 0, $r2 = 0) + { + switch ($side) { + case "top": + case "left": + $tint = array_map([$this, "_tint"], $color); + $this->_border_solid($x, $y, $length, $tint, $widths, $side, $corner_style, $r1, $r2); + break; + + case "bottom": + case "right": + $shade = array_map([$this, "_shade"], $color); + $this->_border_solid($x, $y, $length, $shade, $widths, $side, $corner_style, $r1, $r2); + break; + + default: + return; + } + } + + /** + * Get the dash pattern and cap style for the given border style, width, and + * line length. + * + * The base pattern is adjusted so that it fits the given line length + * symmetrically. + * + * @param string $style + * @param float $width + * @param float $length + * + * @return array + */ + protected function dashPattern(string $style, float $width, float $length): array + { + if ($style === "dashed") { + $w = 3 * $width; + + if ($length < $w) { + $s = $w; + } else { + // Scale dashes and gaps + $r = round($length / $w); + $r = $r % 2 === 0 ? $r + 1 : $r; + $s = $length / $r; + } + + return [[$s], "butt"]; + } + + if ($style === "dotted") { + // Draw circles along the line + // Round caps extend outwards by half line width, so a zero dash + // width results in a circle + $gap = $width <= 1 ? 2 : 1; + $w = ($gap + 1) * $width; + + if ($length < $w) { + $s = $w; + } else { + // Only scale gaps + $l = $length - $width; + $r = max(round($l / $w), 1); + $s = $l / $r; + } + + return [[0, $s], "round"]; + } + + return [[], "butt"]; + } + + /** + * Draws a solid, dotted, or dashed line, observing the border radius + * + * @param float $x + * @param float $y + * @param float $length + * @param array $color + * @param float[] $widths + * @param string $side + * @param string $corner_style + * @param string $pattern_name + * @param float $r1 + * @param float $r2 + */ + protected function _border_line($x, $y, $length, $color, $widths, $side, $corner_style = "bevel", $pattern_name = "none", $r1 = 0, $r2 = 0) + { + /** used by $$side */ + [$top, $right, $bottom, $left] = $widths; + $width = $$side; + + // No need to clip corners if border radius is large enough + $cornerClip = $corner_style === "bevel" && ($r1 < $width || $r2 < $width); + $lineLength = $length - $r1 - $r2; + [$pattern, $cap] = $this->dashPattern($pattern_name, $width, $lineLength); + + // Determine arc border radius for corner arcs + $halfWidth = $width / 2; + $ar1 = max($r1 - $halfWidth, 0); + $ar2 = max($r2 - $halfWidth, 0); + + // Small angle adjustments to prevent the background from shining through + $adj1 = $ar1 / 80; + $adj2 = $ar2 / 80; + + // Adjust line width and corner angles to account for the fact that + // round caps extend outwards. The line is actually only shifted below, + // not shortened, as otherwise the end dash (circle) will vanish + // occasionally + $dl = $cap === "round" ? $halfWidth : 0; + + if ($cap === "round" && $ar1 > 0) { + $adj1 -= rad2deg(asin($halfWidth / $ar1)); + } + if ($cap === "round" && $ar2 > 0) { + $adj2 -= rad2deg(asin($halfWidth / $ar2)); + } + + switch ($side) { + case "top": + if ($cornerClip) { + $points = [ + $x, $y, + $x, $y - 1, // Extend outwards to avoid gaps + $x + $length, $y - 1, // Extend outwards to avoid gaps + $x + $length, $y, + $x + $length - max($right, $r2), $y + max($width, $r2), + $x + max($left, $r1), $y + max($width, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $y += $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x + $r1, $y + $ar1, $ar1, $ar1, 90 - $adj1, 135 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x + $dl + $r1, $y, $x + $dl + $length - $r2, $y, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x + $length - $r2, $y + $ar2, $ar2, $ar2, 45 - $adj2, 90 + $adj2, $color, $width, $pattern, $cap); + } + break; + + case "bottom": + if ($cornerClip) { + $points = [ + $x, $y, + $x, $y + 1, // Extend outwards to avoid gaps + $x + $length, $y + 1, // Extend outwards to avoid gaps + $x + $length, $y, + $x + $length - max($right, $r2), $y - max($width, $r2), + $x + max($left, $r1), $y - max($width, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $y -= $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x + $r1, $y - $ar1, $ar1, $ar1, 225 - $adj1, 270 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x + $dl + $r1, $y, $x + $dl + $length - $r2, $y, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x + $length - $r2, $y - $ar2, $ar2, $ar2, 270 - $adj2, 315 + $adj2, $color, $width, $pattern, $cap); + } + break; + + case "left": + if ($cornerClip) { + $points = [ + $x, $y, + $x - 1, $y, // Extend outwards to avoid gaps + $x - 1, $y + $length, // Extend outwards to avoid gaps + $x, $y + $length, + $x + max($width, $r2), $y + $length - max($bottom, $r2), + $x + max($width, $r1), $y + max($top, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $x += $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x + $ar1, $y + $r1, $ar1, $ar1, 135 - $adj1, 180 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x, $y + $dl + $r1, $x, $y + $dl + $length - $r2, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x + $ar2, $y + $length - $r2, $ar2, $ar2, 180 - $adj2, 225 + $adj2, $color, $width, $pattern, $cap); + } + break; + + case "right": + if ($cornerClip) { + $points = [ + $x, $y, + $x + 1, $y, // Extend outwards to avoid gaps + $x + 1, $y + $length, // Extend outwards to avoid gaps + $x, $y + $length, + $x - max($width, $r2), $y + $length - max($bottom, $r2), + $x - max($width, $r1), $y + max($top, $r1) + ]; + $this->_canvas->clipping_polygon($points); + } + + $x -= $halfWidth; + + if ($ar1 > 0 && $adj1 > -22.5) { + $this->_canvas->arc($x - $ar1, $y + $r1, $ar1, $ar1, 0 - $adj1, 45 + $adj1, $color, $width, $pattern, $cap); + } + + if ($lineLength > 0) { + $this->_canvas->line($x, $y + $dl + $r1, $x, $y + $dl + $length - $r2, $color, $width, $pattern, $cap); + } + + if ($ar2 > 0 && $adj2 > -22.5) { + $this->_canvas->arc($x - $ar2, $y + $length - $r2, $ar2, $ar2, 315 - $adj2, 360 + $adj2, $color, $width, $pattern, $cap); + } + break; + } + + if ($cornerClip) { + $this->_canvas->clipping_end(); + } + } + + /** + * @param float $opacity + */ + protected function _set_opacity(float $opacity): void + { + if ($opacity >= 0.0 && $opacity <= 1.0) { + $this->_canvas->set_opacity($opacity); + } + } + + /** + * Add a named destination if the element has an ID or is an anchor element + * with `name` attribute. + * + * @param DOMElement $node + */ + protected function addNamedDest(DOMElement $node): void + { + $id = $node->getAttribute("id"); + if ($id !== "") { + $this->_canvas->add_named_dest($id); + } + + if ($node->nodeName === "a") { + $name = $node->getAttribute("name"); + if ($name !== "") { + $this->_canvas->add_named_dest($name); + } + } + } + + /** + * Add a hyperlink if the element is an anchor element with `href` + * attribute. + * + * @param DOMElement $node + * @param float[] $borderBox + */ + protected function addHyperlink(DOMElement $node, array $borderBox): void + { + if ($node->nodeName === "a" && ($href = $node->getAttribute("href")) !== "") { + [$x, $y, $w, $h] = $borderBox; + $dompdf = $this->_dompdf; + $href = Helpers::build_url( + $dompdf->getProtocol(), + $dompdf->getBaseHost(), + $dompdf->getBasePath(), + $href, + $dompdf->getOptions()->getChroot() + ) ?? $href; + $this->_canvas->add_link($href, $x, $y, $w, $h); + } + } + + /** + * @param float[] $box + * @param array|string $color + * @param array $style + */ + protected function debugLayout(array $box, $color = "red", array $style = []): void + { + $this->_canvas->rectangle($box[0], $box[1], $box[2], $box[3], Color::parse($color), 0.1, $style); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/Block.php b/application/third_party/dompdf-3.1.4/src/Renderer/Block.php new file mode 100644 index 0000000..ab2768d --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/Block.php @@ -0,0 +1,81 @@ +get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + [$x, $y, $w, $h] = $frame->get_border_box(); + + if ($node->nodeName === "body") { + // Margins should be fully resolved at this point + $mt = $style->margin_top; + $mb = $style->margin_bottom; + $h = $frame->get_containing_block("h") - $mt - $mb; + } + + $border_box = [$x, $y, $w, $h]; + + // Draw our background, border and content + $this->_render_background($frame, $border_box); + $this->_render_border($frame, $border_box); + $this->_render_outline($frame, $border_box); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + $this->debugBlockLayout($frame, "red", false); + } + + /** + * @param Frame $frame + * @param array|string $color + * @param bool $lines + */ + protected function debugBlockLayout(Frame $frame, $color, bool $lines = false): void + { + $options = $this->_dompdf->getOptions(); + $debugLayout = $options->getDebugLayout(); + + if (!$debugLayout) { + return; + } + + if ($options->getDebugLayoutBlocks()) { + $this->debugLayout($frame->get_border_box(), $color); + + if ($options->getDebugLayoutPaddingBox()) { + $this->debugLayout($frame->get_padding_box(), $color, [0.5, 0.5]); + } + } + + if ($lines && $options->getDebugLayoutLines() && $frame instanceof BlockFrameDecorator) { + [$cx, , $cw] = $frame->get_content_box(); + + foreach ($frame->get_line_boxes() as $line) { + $lw = $cw - $line->left - $line->right; + $this->debugLayout([$cx + $line->left, $line->y, $lw, $line->h], "orange"); + } + } + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/Image.php b/application/third_party/dompdf-3.1.4/src/Renderer/Image.php new file mode 100644 index 0000000..1373bb0 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/Image.php @@ -0,0 +1,73 @@ +get_style(); + $node = $frame->get_node(); + $border_box = $frame->get_border_box(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + // Render background & borders + $this->_render_background($frame, $border_box); + $this->_render_border($frame, $border_box); + $this->_render_outline($frame, $border_box); + + $content_box = $frame->get_content_box(); + [$x, $y, $w, $h] = $content_box; + + $src = $frame->get_image_url(); + + if (Cache::is_broken($src) && ($alt = $node->getAttribute("alt")) !== "") { + $font = $style->font_family; + $size = $style->font_size; + $word_spacing = $style->word_spacing; + $letter_spacing = $style->letter_spacing; + + $this->_canvas->text( + $x, + $y, + $alt, + $font, + $size, + $style->color, + $word_spacing, + $letter_spacing + ); + } elseif ($w > 0 && $h > 0) { + if ($style->has_border_radius()) { + [$tl, $tr, $br, $bl] = $style->resolve_border_radius($border_box, $content_box); + $this->_canvas->clipping_roundrectangle($x, $y, $w, $h, $tl, $tr, $br, $bl); + } + + $this->_canvas->image($src, $x, $y, $w, $h, $style->image_resolution); + + if ($style->has_border_radius()) { + $this->_canvas->clipping_end(); + } + } + + $this->addNamedDest($node); + $this->debugBlockLayout($frame, "blue"); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/Inline.php b/application/third_party/dompdf-3.1.4/src/Renderer/Inline.php new file mode 100644 index 0000000..5d9e284 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/Inline.php @@ -0,0 +1,121 @@ +get_first_child(); + while ($child && !$child->is_in_flow()) { + $child = $child->get_next_sibling(); + } + + if (!$child) { + return; // No children, no service + } + + $style = $frame->get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + // Draw background & border behind each child. To do this, we need to + // to figure out just how much space each child takes. Retrieve the + // position of the first child again, to account for text and vertical + // alignment + [$x, $y] = $child->get_position(); + [$w, $h] = $this->get_child_size($frame); + + [, , $cbw] = $frame->get_containing_block(); + $margin_left = $style->length_in_pt($style->margin_left, $cbw); + $pt = $style->length_in_pt($style->padding_top, $cbw); + $pb = $style->length_in_pt($style->padding_bottom, $cbw); + + // Make sure that border and background start inside the left margin + // Extend the drawn box by border and padding in vertical direction, as + // these do not affect layout + // FIXME: Using a small vertical offset of a fraction of the height here + // to work around the vertical position being slightly off in general + $x += $margin_left; + $y -= $style->border_top_width + $pt - ($h * 0.1); + $h += $style->border_top_width + $pt + $style->border_bottom_width + $pb; + + $border_box = [$x, $y, $w, $h]; + $this->_render_background($frame, $border_box); + $this->_render_border($frame, $border_box); + $this->_render_outline($frame, $border_box); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + + $options = $this->_dompdf->getOptions(); + + if ($options->getDebugLayout() && $options->getDebugLayoutInline()) { + $this->debugLayout($border_box, "blue"); + + if ($options->getDebugLayoutPaddingBox()) { + $padding_box = [ + $x + $style->border_left_width, + $y + $style->border_top_width, + $w - $style->border_left_width - $style->border_right_width, + $h - $style->border_top_width - $style->border_bottom_width + ]; + $this->debugLayout($padding_box, "blue", [0.5, 0.5]); + } + } + } + + protected function get_child_size(Frame $frame): array + { + $w = 0.0; + $h = 0.0; + + foreach ($frame->get_children() as $child) { + if (!$child->is_in_flow()) { + continue; + } + + // Exclude trailing white space + if ($child->get_node()->nodeValue === " " + && $child->get_prev_sibling() && !$child->get_next_sibling() + ) { + break; + } + + $style = $child->get_style(); + $auto_width = $style->width === "auto"; + $auto_height = $style->height === "auto"; + [, , $child_w, $child_h] = $child->get_border_box(); + + if ($auto_width || $auto_height) { + [$child_w2, $child_h2] = $this->get_child_size($child); + + if ($auto_width) { + $child_w = $child_w2; + } + + if ($auto_height) { + $child_h = $child_h2; + } + } + + $w += $child_w; + $h = max($h, $child_h); + } + + return [$w, $h]; + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/ListBullet.php b/application/third_party/dompdf-3.1.4/src/Renderer/ListBullet.php new file mode 100644 index 0000000..dadfe2b --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/ListBullet.php @@ -0,0 +1,213 @@ +get_parent(); + $style = $frame->get_style(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + // Don't render bullets twice if the list item was split + if ($li->is_split_off) { + return; + } + + $font_family = $style->font_family; + $font_size = $style->font_size; + $baseline = $this->_canvas->get_font_baseline($font_family, $font_size); + + // Handle list-style-image + // If list style image is requested but missing, fall back to predefined types + if ($frame instanceof ListBulletImage && !Cache::is_broken($img = $frame->get_image_url())) { + [$x, $y] = $frame->get_position(); + $w = $frame->get_width(); + $h = $frame->get_height(); + $y += $baseline - $h; + + $this->_canvas->image($img, $x, $y, $w, $h); + } else { + $bullet_style = $style->list_style_type; + + switch ($bullet_style) { + case "disc": + case "circle": + [$x, $y] = $frame->get_position(); + $offset = $font_size * ListBulletFrameDecorator::BULLET_OFFSET; + $r = ($font_size * ListBulletFrameDecorator::BULLET_SIZE) / 2; + $x += $r; + $y += $baseline - $r - $offset; + $o = $font_size * ListBulletFrameDecorator::BULLET_THICKNESS; + $this->_canvas->circle($x, $y, $r, $style->color, $o, null, $bullet_style !== "circle"); + break; + + case "square": + [$x, $y] = $frame->get_position(); + $offset = $font_size * ListBulletFrameDecorator::BULLET_OFFSET; + $w = $font_size * ListBulletFrameDecorator::BULLET_SIZE; + $y += $baseline - $w - $offset; + $this->_canvas->filled_rectangle($x, $y, $w, $w, $style->color); + break; + + default: + case "decimal": + case "decimal-leading-zero": + case "lower-alpha": + case "lower-latin": + case "lower-roman": + case "lower-greek": + case "upper-alpha": + case "upper-latin": + case "upper-roman": + $pad = null; + if ($bullet_style === "decimal-leading-zero") { + $pad = strlen($li->get_parent()->get_node()->getAttribute("dompdf-children-count")); + } + + $node = $frame->get_node(); + + if (!$node->hasAttribute("dompdf-counter")) { + return; + } + + $index = (int) $node->getAttribute("dompdf-counter"); + $text = $this->make_counter($index, $bullet_style, $pad); + + $word_spacing = $style->word_spacing; + $letter_spacing = $style->letter_spacing; + $text_width = $this->_dompdf->getFontMetrics()->getTextWidth($text, $font_family, $font_size, $word_spacing, $letter_spacing); + + [$x, $y] = $frame->get_position(); + // Correct for static frame width applied by positioner + $x += $frame->get_width() - $text_width; + + $this->_canvas->text($x, $y, $text, + $font_family, $font_size, + $style->color, $word_spacing, $letter_spacing); + break; + + case "none": + break; + } + } + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/TableCell.php b/application/third_party/dompdf-3.1.4/src/Renderer/TableCell.php new file mode 100644 index 0000000..fbf9178 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/TableCell.php @@ -0,0 +1,189 @@ +get_style(); + $node = $frame->get_node(); + + if (trim($node->nodeValue) === "" && $style->empty_cells === "hide") { + return; + } + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + $border_box = $frame->get_border_box(); + $table = Table::find_parent_table($frame); + if ($table === null) { + throw new Exception("Parent table not found for table cell"); + } + + if ($table->get_style()->border_collapse !== "collapse") { + $this->_render_background($frame, $border_box); + $this->_render_border($frame, $border_box); + $this->_render_outline($frame, $border_box); + } else { + // The collapsed case is slightly complicated... + + $cells = $table->get_cellmap()->get_spanned_cells($frame); + + if (is_null($cells)) { + return; + } + + // Render the background to the padding box, as the cells are + // rendered individually one after another, and we don't want the + // background to overlap an adjacent border + $padding_box = $frame->get_padding_box(); + + $this->_render_background($frame, $padding_box); + $this->_render_collapsed_border($frame, $table); + + // FIXME: Outline should be drawn over other cells + $this->_render_outline($frame, $border_box); + } + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + $this->debugBlockLayout($frame, "red", false); + } + + /** + * @param Frame $frame + * @param Table $table + */ + protected function _render_collapsed_border(Frame $frame, Table $table): void + { + $cellmap = $table->get_cellmap(); + $cells = $cellmap->get_spanned_cells($frame); + $num_rows = $cellmap->get_num_rows(); + $num_cols = $cellmap->get_num_cols(); + + [$table_x, $table_y] = $table->get_position(); + + // Determine the top row spanned by this cell + $i = $cells["rows"][0]; + $top_row = $cellmap->get_row($i); + + // Determine if this cell borders on the bottom of the table. If so, + // then we draw its bottom border. Otherwise the next row down will + // draw its top border instead. + if (in_array($num_rows - 1, $cells["rows"])) { + $draw_bottom = true; + $bottom_row = $cellmap->get_row($num_rows - 1); + } else { + $draw_bottom = false; + } + + // Draw the horizontal borders + foreach ($cells["columns"] as $j) { + $bp = $cellmap->get_border_properties($i, $j); + $col = $cellmap->get_column($j); + + $x = $table_x + $col["x"] - $bp["left"]["width"] / 2; + $y = $table_y + $top_row["y"] - $bp["top"]["width"] / 2; + $w = $col["used-width"] + ($bp["left"]["width"] + $bp["right"]["width"]) / 2; + + if ($bp["top"]["width"] > 0) { + $widths = [ + (float)$bp["top"]["width"], + (float)$bp["right"]["width"], + (float)$bp["bottom"]["width"], + (float)$bp["left"]["width"] + ]; + + $method = "_border_" . $bp["top"]["style"]; + $this->$method($x, $y, $w, $bp["top"]["color"], $widths, "top", "square"); + } + + if ($draw_bottom) { + $bp = $cellmap->get_border_properties($num_rows - 1, $j); + if ($bp["bottom"]["width"] <= 0) { + continue; + } + + $widths = [ + (float)$bp["top"]["width"], + (float)$bp["right"]["width"], + (float)$bp["bottom"]["width"], + (float)$bp["left"]["width"] + ]; + + $y = $table_y + $bottom_row["y"] + $bottom_row["height"] + $bp["bottom"]["width"] / 2; + + $method = "_border_" . $bp["bottom"]["style"]; + $this->$method($x, $y, $w, $bp["bottom"]["color"], $widths, "bottom", "square"); + } + } + + $j = $cells["columns"][0]; + $left_col = $cellmap->get_column($j); + + if (in_array($num_cols - 1, $cells["columns"])) { + $draw_right = true; + $right_col = $cellmap->get_column($num_cols - 1); + } else { + $draw_right = false; + } + + // Draw the vertical borders + foreach ($cells["rows"] as $i) { + $bp = $cellmap->get_border_properties($i, $j); + $row = $cellmap->get_row($i); + + $x = $table_x + $left_col["x"] - $bp["left"]["width"] / 2; + $y = $table_y + $row["y"] - $bp["top"]["width"] / 2; + $h = $row["height"] + ($bp["top"]["width"] + $bp["bottom"]["width"]) / 2; + + if ($bp["left"]["width"] > 0) { + $widths = [ + (float)$bp["top"]["width"], + (float)$bp["right"]["width"], + (float)$bp["bottom"]["width"], + (float)$bp["left"]["width"] + ]; + + $method = "_border_" . $bp["left"]["style"]; + $this->$method($x, $y, $h, $bp["left"]["color"], $widths, "left", "square"); + } + + if ($draw_right) { + $bp = $cellmap->get_border_properties($i, $num_cols - 1); + if ($bp["right"]["width"] <= 0) { + continue; + } + + $widths = [ + (float)$bp["top"]["width"], + (float)$bp["right"]["width"], + (float)$bp["bottom"]["width"], + (float)$bp["left"]["width"] + ]; + + $x = $table_x + $right_col["x"] + $right_col["used-width"] + $bp["right"]["width"] / 2; + + $method = "_border_" . $bp["right"]["style"]; + $this->$method($x, $y, $h, $bp["right"]["color"], $widths, "right", "square"); + } + } + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/TableRow.php b/application/third_party/dompdf-3.1.4/src/Renderer/TableRow.php new file mode 100644 index 0000000..b1608e8 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/TableRow.php @@ -0,0 +1,40 @@ +get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + $border_box = $frame->get_border_box(); + + // FIXME: Render background onto the area consisting of all spanned + // cells. In the separated border model, the border-spacing area should + // be left out. Currently, the background is inherited by the table + // cells instead, which does not handle transparent backgrounds and + // background images correctly. + // See https://www.w3.org/TR/CSS21/tables.html#table-layers + + $this->_render_outline($frame, $border_box); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/TableRowGroup.php b/application/third_party/dompdf-3.1.4/src/Renderer/TableRowGroup.php new file mode 100644 index 0000000..eb5d23b --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/TableRowGroup.php @@ -0,0 +1,40 @@ +get_style(); + $node = $frame->get_node(); + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + $border_box = $frame->get_border_box(); + + // FIXME: Render background onto the area consisting of all spanned + // cells. In the separated border model, the border-spacing area should + // be left out. Currently, the background is inherited by the table + // cells instead, which does not handle transparent backgrounds and + // background images correctly. + // See https://www.w3.org/TR/CSS21/tables.html#table-layers + + $this->_render_outline($frame, $border_box); + + $this->addNamedDest($node); + $this->addHyperlink($node, $border_box); + } +} diff --git a/application/third_party/dompdf-3.1.4/src/Renderer/Text.php b/application/third_party/dompdf-3.1.4/src/Renderer/Text.php new file mode 100644 index 0000000..656f3c9 --- /dev/null +++ b/application/third_party/dompdf-3.1.4/src/Renderer/Text.php @@ -0,0 +1,161 @@ +_canvas, "get_cpdf" ) + //- For cpdf these can and must stay 0, because font metrics are used directly. + //- For other renderers, if different values are wanted, separate the parameter sets. + // But $size and $size-$height seem to be accurate enough + + /** Relative to bottom of text, as fraction of height */ + const UNDERLINE_OFFSET = 0.0; + + /** Relative to top of text */ + const OVERLINE_OFFSET = 0.0; + + /** Relative to centre of text. */ + const LINETHROUGH_OFFSET = 0.0; + + /** How far to extend lines past either end, in pt */ + const DECO_EXTENSION = 0.0; + + /** + * @param \Dompdf\FrameDecorator\Text $frame + */ + function render(Frame $frame) + { + $style = $frame->get_style(); + $text = $frame->get_text(); + + if ($text === "") { + return; + } + + $this->_set_opacity($frame->get_opacity($style->opacity)); + + [$x, $y] = $frame->get_position(); + $cb = $frame->get_containing_block(); + + $ml = $style->margin_left; + $pl = $style->padding_left; + $bl = $style->border_left_width; + $x += (float) $style->length_in_pt([$ml, $pl, $bl], $cb["w"]); + + $font = $style->font_family; + $size = $style->font_size; + $frame_font_size = $frame->get_dompdf()->getFontMetrics()->getFontHeight($font, $size); + $word_spacing = $frame->get_text_spacing() + $style->word_spacing; + $letter_spacing = $style->letter_spacing; + $width = (float) $style->width; + + /*$text = str_replace( + array("{PAGE_NUM}"), + array($this->_canvas->get_page_number()), + $text + );*/ + + $this->_canvas->text($x, $y, $text, + $font, $size, + $style->color, $word_spacing, $letter_spacing); + + $line = $frame->get_containing_line(); + + // FIXME Instead of using the tallest frame to position, + // the decoration, the text should be well placed + if (false && $line->tallest_frame) { + $base_frame = $line->tallest_frame; + $style = $base_frame->get_style(); + $size = $style->font_size; + } + + $line_thickness = $size * self::DECO_THICKNESS; + $underline_offset = $size * self::UNDERLINE_OFFSET; + $overline_offset = $size * self::OVERLINE_OFFSET; + $linethrough_offset = $size * self::LINETHROUGH_OFFSET; + $underline_position = -0.08; + + if ($this->_canvas instanceof CPDF) { + $cpdf_font = $this->_canvas->get_cpdf()->fonts[$style->font_family]; + + if (isset($cpdf_font["UnderlinePosition"])) { + $underline_position = $cpdf_font["UnderlinePosition"] / 1000; + } + + if (isset($cpdf_font["UnderlineThickness"])) { + $line_thickness = $size * ($cpdf_font["UnderlineThickness"] / 1000); + } + } + + $descent = $size * $underline_position; + $base = $frame_font_size; + + // Handle text decoration: + // http://www.w3.org/TR/CSS21/text.html#propdef-text-decoration + + // Draw all applicable text-decorations. Start with the root and work our way down. + $p = $frame; + $stack = []; + while ($p = $p->get_parent()) { + $stack[] = $p; + } + + while (isset($stack[0])) { + $f = array_pop($stack); + + if (($text_deco = $f->get_style()->text_decoration) === "none") { + continue; + } + + $deco_y = $y; //$line->y; + $color = $f->get_style()->color; + + switch ($text_deco) { + default: + continue 2; + + case "underline": + $deco_y += $base - $descent + $underline_offset + $line_thickness / 2; + break; + + case "overline": + $deco_y += $overline_offset + $line_thickness / 2; + break; + + case "line-through": + $deco_y += $base * 0.7 + $linethrough_offset; + break; + } + + $dx = 0; + $x1 = $x - self::DECO_EXTENSION; + $x2 = $x + $width + $dx + self::DECO_EXTENSION; + $this->_canvas->line($x1, $deco_y, $x2, $deco_y, $color, $line_thickness); + } + + $options = $this->_dompdf->getOptions(); + + if ($options->getDebugLayout() && $options->getDebugLayoutLines()) { + $fontMetrics = $this->_dompdf->getFontMetrics(); + $textWidth = $fontMetrics->getTextWidth($text, $font, $size, $word_spacing, $letter_spacing); + $this->debugLayout([$x, $y, $textWidth, $frame_font_size], "orange", [0.5, 0.5]); + } + } +} diff --git a/application/third_party/fpdf/FAQ.htm b/application/third_party/fpdf/FAQ.htm new file mode 100644 index 0000000..dd263ec --- /dev/null +++ b/application/third_party/fpdf/FAQ.htm @@ -0,0 +1,270 @@ + + + + +FAQ + + + + +

FAQ

+ + +
    +
  • +

    1. What's exactly the license of FPDF? Are there any usage restrictions?

    +FPDF is released under a permissive license: there is no usage restriction. You may embed it +freely in your application (commercial or not), with or without modifications. +
  • + +
  • +

    2. I get the following error when I try to generate a PDF: Some data has already been output, can't send PDF file

    +You must send nothing to the browser except the PDF itself: no HTML, no space, no carriage return. A common +case is having extra blank at the end of an included script file.
    +
    +The message may be followed by this indication:
    +
    +(output started at script.php:X)
    +
    +which gives you exactly the script and line number responsible for the output. If you don't see it, +try adding this line at the very beginning of your script: +
    +
    ob_end_clean();
    +
    +
  • + +
  • +

    3. Accented letters are replaced by some strange characters like é.

    +Don't use UTF-8 with the standard fonts; they expect text encoded in windows-1252. +You can perform a conversion with iconv: +
    +
    $str = iconv('UTF-8', 'windows-1252', $str);
    +
    +Or with mbstring: +
    +
    $str = mb_convert_encoding($str, 'windows-1252', 'UTF-8');
    +
    +In case you need characters outside windows-1252, take a look at tutorial #7 or +tFPDF. +
  • + +
  • +

    4. I try to display the Euro symbol but it doesn't work.

    +The standard fonts have the Euro character at position 128. You can define a constant like this +for convenience: +
    +
    define('EURO', chr(128));
    +
    +
  • + +
  • +

    5. I try to display a variable in the Header method but nothing prints.

    +You have to use the global keyword to access global variables, for example: +
    +
    function Header()
    +{
    +    global $title;
    +
    +    $this->SetFont('Arial', 'B', 15);
    +    $this->Cell(0, 10, $title, 1, 1, 'C');
    +}
    +
    +$title = 'My title';
    +
    +Alternatively, you can use an object property: +
    +
    function Header()
    +{
    +    $this->SetFont('Arial', 'B', 15);
    +    $this->Cell(0, 10, $this->title, 1, 1, 'C');
    +}
    +
    +$pdf->title = 'My title';
    +
    +
  • + +
  • +

    6. I have defined the Header and Footer methods in my PDF class but nothing shows.

    +You have to create an object from the PDF class, not FPDF: +
    +
    $pdf = new PDF();
    +
    +
  • + +
  • +

    7. I can't make line breaks work. I put \n in the string printed by MultiCell but it doesn't work.

    +You have to enclose your string with double quotes, not single ones. +
  • + +
  • +

    8. I use jQuery to generate the PDF but it doesn't show.

    +Don't use an AJAX request to retrieve the PDF. +
  • + +
  • +

    9. I draw a frame with very precise dimensions, but when printed I notice some differences.

    +To respect dimensions, select "None" for the Page Scaling setting instead of "Shrink to Printable Area" in the print dialog box. +
  • + +
  • +

    10. I'd like to use the whole surface of the page, but when printed I always have some margins. How can I get rid of them?

    +Printers have physical margins (different depending on the models); it is therefore impossible to remove +them and print on the whole surface of the paper. +
  • + +
  • +

    11. How can I put a background in my PDF?

    +For a picture, call Image() in the Header() method, before any other output. To set a background color, use Rect(). +
  • + +
  • +

    12. How can I set a specific header or footer on the first page?

    +Just test the page number: +
    +
    function Header()
    +{
    +    if($this->PageNo()==1)
    +    {
    +        //First page
    +        ...
    +    }
    +    else
    +    {
    +        //Other pages
    +        ...
    +    }
    +}
    +
    +
  • + +
  • +

    13. I'd like to use extensions provided by different scripts. How can I combine them?

    +Use an inheritance chain. If you have two classes, say A in a.php: +
    +
    require('fpdf.php');
    +
    +class A extends FPDF
    +{
    +...
    +}
    +
    +and B in b.php: +
    +
    require('fpdf.php');
    +
    +class B extends FPDF
    +{
    +...
    +}
    +
    +then make B extend A: +
    +
    require('a.php');
    +
    +class B extends A
    +{
    +...
    +}
    +
    +and make your own class extend B: +
    +
    require('b.php');
    +
    +class PDF extends B
    +{
    +...
    +}
    +
    +$pdf = new PDF();
    +
    +
  • + +
  • +

    14. How can I open the PDF in a new tab?

    +Just do the same as you would for an HTML page or anything else: add a target="_blank" to your link or form. +
  • + +
  • +

    15. How can I send the PDF by email?

    +As for any other file, but an easy way is to use PHPMailer and +its in-memory attachment: +
    +
    $mail = new PHPMailer();
    +...
    +$doc = $pdf->Output('S');
    +$mail->AddStringAttachment($doc, 'doc.pdf', 'base64', 'application/pdf');
    +$mail->Send();
    +
    +
  • + +
  • +

    16. What's the limit of the file sizes I can generate with FPDF?

    +There is no particular limit. There are some constraints, however: +
    +
    +- There is usually a maximum memory size allocated to PHP scripts. For very big documents, +especially with images, the limit may be reached (the file being built in memory). The +parameter is configured in the php.ini file. +
    +
    +- The maximum execution time allocated to scripts defaults to 30 seconds. This limit can of course +be easily reached. It is configured in php.ini and may be altered dynamically with set_time_limit(). +
    +
    +You can work around the memory limit with this script. +
  • + +
  • +

    17. Can I modify a PDF with FPDF?

    +It's possible to import pages from an existing PDF document thanks to the +FPDI extension. +Then you can add some content to them. +
  • + +
  • +

    18. I'd like to make a search engine in PHP and index PDF files. Can I do it with FPDF?

    +No. But a GPL C utility does exist, pdftotext, which is able to extract the textual content from a PDF. +It's provided with the Xpdf package. +
  • + +
  • +

    19. Can I convert an HTML page to PDF with FPDF?

    +Not real-world pages. But a GPL C utility does exist, HTMLDOC, +which allows to do it and gives good results. +
  • + +
  • +

    20. Can I concatenate PDF files with FPDF?

    +Not directly, but it's possible to use FPDI +to perform that task. Some free command-line tools also exist: +pdftk and +mbtPdfAsm. +
  • +
+ + diff --git a/application/third_party/fpdf/changelog.htm b/application/third_party/fpdf/changelog.htm new file mode 100644 index 0000000..1c111fa --- /dev/null +++ b/application/third_party/fpdf/changelog.htm @@ -0,0 +1,188 @@ + + + + +Changelog + + + + +

Changelog

+
+
v1.86 (2023-06-25)
+
+- Added a parameter to AddFont() to specify the directory where to load the font definition file.
+- Fixed a bug related to the PDF creation date.
+
+
v1.85 (2022-11-10)
+
+- Removed deprecation notices on PHP 8.2.
+- Removed notices when passing null values instead of strings.
+- The FPDF_VERSION constant was replaced by a class constant.
+- The creation date of the PDF now includes the timezone.
+- The content-type is now always application/pdf, even for downloads.
+
+
v1.84 (2021-08-28)
+
+- Fixed an issue related to annotations.
+
+
v1.83 (2021-04-18)
+
+- Fixed an issue related to annotations.
+
+
v1.82 (2019-12-07)
+
+- Removed a deprecation notice on PHP 7.4.
+
+
v1.81 (2015-12-20)
+
+- Added GetPageWidth() and GetPageHeight().
+- Fixed a bug in SetXY().
+
+
v1.8 (2015-11-29)
+
+- PHP 5.1.0 or higher is now required.
+- The MakeFont utility now subsets fonts, which can greatly reduce font sizes.
+- Added ToUnicode CMaps to improve text extraction.
+- Added a parameter to AddPage() to rotate the page.
+- Added a parameter to SetY() to indicate whether the x position should be reset or not.
+- Added a parameter to Output() to specify the encoding of the name, and special characters are now properly encoded. Additionally the order of the first two parameters was reversed to be more logical (however the old order is still supported for compatibility).
+- The Error() method now throws an exception.
+- Adding contents before the first AddPage() or after Close() now raises an error.
+- Outputting text with no font selected now raises an error.
+
+
v1.7 (2011-06-18)
+
+- The MakeFont utility has been completely rewritten and doesn't depend on ttf2pt1 anymore.
+- Alpha channel is now supported for PNGs.
+- When inserting an image, it's now possible to specify its resolution.
+- Default resolution for images was increased from 72 to 96 dpi.
+- When inserting a GIF image, no temporary file is used anymore if the PHP version is 5.1 or higher.
+- When output buffering is enabled and the PDF is about to be sent, the buffer is now cleared if it contains only a UTF-8 BOM and/or whitespace (instead of throwing an error).
+- Symbol and ZapfDingbats fonts now support underline style.
+- Custom page sizes are now checked to ensure that width is smaller than height.
+- Standard font files were changed to use the same format as user fonts.
+- A bug in the embedding of Type1 fonts was fixed.
+- A bug related to SetDisplayMode() and the current locale was fixed.
+- A display issue occurring with the Adobe Reader X plug-in was fixed.
+- An issue related to transparency with some versions of Adobe Reader was fixed.
+- The Content-Length header was removed because it caused an issue when the HTTP server applies compression.
+
+
v1.6 (2008-08-03)
+
+- PHP 4.3.10 or higher is now required.
+- GIF image support.
+- Images can now trigger page breaks.
+- Possibility to have different page formats in a single document.
+- Document properties (author, creator, keywords, subject and title) can now be specified in UTF-8.
+- Fixed a bug: when a PNG was inserted through a URL, an error sometimes occurred.
+- An automatic page break in Header() doesn't cause an infinite loop any more.
+- Removed some warning messages appearing with recent PHP versions.
+- Added HTTP headers to reduce problems with IE.
+
+
v1.53 (2004-12-31)
+
+- When the font subdirectory is in the same directory as fpdf.php, it's no longer necessary to define the FPDF_FONTPATH constant.
+- The array $HTTP_SERVER_VARS is no longer used. It could cause trouble on PHP5-based configurations with the register_long_arrays option disabled.
+- Fixed a problem related to Type1 font embedding which caused trouble to some PDF processors.
+- The file name sent to the browser could not contain a space character.
+- The Cell() method could not print the number 0 (you had to pass the string '0').
+
+
v1.52 (2003-12-30)
+
+- Image() now displays the image at 72 dpi if no dimension is given.
+- Output() takes a string as second parameter to indicate destination.
+- Open() is now called automatically by AddPage().
+- Inserting remote JPEG images doesn't generate an error any longer.
+- Decimal separator is forced to dot in the constructor.
+- Added several encodings (Turkish, Thai, Hebrew, Ukrainian and Vietnamese).
+- The last line of a right-aligned MultiCell() was not correctly aligned if it was terminated by a carriage return.
+- No more error message about already sent headers when outputting the PDF to the standard output from the command line.
+- The underlining was going too far for text containing characters \, ( or ).
+- $HTTP_ENV_VARS has been replaced by $HTTP_SERVER_VARS.
+
+
v1.51 (2002-08-03)
+
+- Type1 font support.
+- Added Baltic encoding.
+- The class now works internally in points with the origin at the bottom in order to avoid two bugs occurring with Acrobat 5:
  * The line thickness was too large when printed on Windows 98 SE and ME.
  * TrueType fonts didn't appear immediately inside the plug-in (a substitution font was used), one had to cause a window refresh to make them show up.
+- It's no longer necessary to set the decimal separator as dot to produce valid documents.
+- The clickable area in a cell was always on the left independently from the text alignment.
+- JPEG images in CMYK mode appeared in inverted colors.
+- Transparent PNG images in grayscale or true color mode were incorrectly handled.
+- Adding new fonts now works correctly even with the magic_quotes_runtime option set to on.
+
+
v1.5 (2002-05-28)
+
+- TrueType font (AddFont()) and encoding support (Western and Eastern Europe, Cyrillic and Greek).
+- Added Write() method.
+- Added underlined style.
+- Internal and external link support (AddLink(), SetLink(), Link()).
+- Added right margin management and methods SetRightMargin(), SetTopMargin().
+- Modification of SetDisplayMode() to select page layout.
+- The border parameter of MultiCell() now lets choose borders to draw as Cell().
+- When a document contains no page, Close() now calls AddPage() instead of causing a fatal error.
+
+
v1.41 (2002-03-13)
+
+- Fixed SetDisplayMode() which no longer worked (the PDF viewer used its default display).
+
+
v1.4 (2002-03-02)
+
+- PHP3 is no longer supported.
+- Page compression (SetCompression()).
+- Choice of page format and possibility to change orientation inside document.
+- Added AcceptPageBreak() method.
+- Ability to print the total number of pages (AliasNbPages()).
+- Choice of cell borders to draw.
+- New mode for Cell(): the current position can now move under the cell.
+- Ability to include an image by specifying height only (width is calculated automatically).
+- Fixed a bug: when a justified line triggered a page break, the footer inherited the corresponding word spacing.
+
+
v1.31 (2002-01-12)
+
+- Fixed a bug in drawing frame with MultiCell(): the last line always started from the left margin.
+- Removed Expires HTTP header (gives trouble in some situations).
+- Added Content-disposition HTTP header (seems to help in some situations).
+
+
v1.3 (2001-12-03)
+
+- Line break and text justification support (MultiCell()).
+- Color support (SetDrawColor(), SetFillColor(), SetTextColor()). Possibility to draw filled rectangles and paint cell background.
+- A cell whose width is declared null extends up to the right margin of the page.
+- Line width is now retained from page to page and defaults to 0.2 mm.
+- Added SetXY() method.
+- Fixed a passing by reference done in a deprecated manner for PHP4.
+
+
v1.2 (2001-11-11)
+
+- Added font metric files and GetStringWidth() method.
+- Centering and right-aligning text in cells.
+- Display mode control (SetDisplayMode()).
+- Added methods to set document properties (SetAuthor(), SetCreator(), SetKeywords(), SetSubject(), SetTitle()).
+- Possibility to force PDF download by browser.
+- Added SetX() and GetX() methods.
+- During automatic page break, current abscissa is now retained.
+
+
v1.11 (2001-10-20)
+
+- PNG support doesn't require PHP4/zlib any more. Data are now put directly into PDF without any decompression/recompression stage.
+- Image insertion now works correctly even with magic_quotes_runtime option set to on.
+
+
v1.1 (2001-10-07)
+
+- JPEG and PNG image support.
+
+
v1.01 (2001-10-03)
+
+- Fixed a bug involving page break: in case when Header() doesn't specify a font, the one from previous page was not restored and produced an incorrect document.
+
+
v1.0 (2001-09-17)
+
+- First version.
+
+
+ + diff --git a/application/third_party/fpdf/doc/__construct.htm b/application/third_party/fpdf/doc/__construct.htm new file mode 100644 index 0000000..c79165e --- /dev/null +++ b/application/third_party/fpdf/doc/__construct.htm @@ -0,0 +1,63 @@ + + + + +__construct + + + +

__construct

+__construct([string orientation [, string unit [, mixed size]]]) +

Description

+This is the class constructor. It allows to set up the page size, the orientation and the +unit of measure used in all methods (except for font sizes). +

Parameters

+
+
orientation
+
+Default page orientation. Possible values are (case insensitive): +
    +
  • P or Portrait
  • +
  • L or Landscape
  • +
+Default value is P. +
+
unit
+
+User unit. Possible values are: +
    +
  • pt: point
  • +
  • mm: millimeter
  • +
  • cm: centimeter
  • +
  • in: inch
  • +
+A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This +is a very common unit in typography; font sizes are expressed in that unit. +
+
+Default value is mm. +
+
size
+
+The size used for pages. It can be either one of the following values (case insensitive): +
    +
  • A3
  • +
  • A4
  • +
  • A5
  • +
  • Letter
  • +
  • Legal
  • +
+or an array containing the width and the height (expressed in the unit given by unit).
+
+Default value is A4. +
+
+

Example

+Document with a custom 100x150 mm page size: +
+
$pdf = new FPDF('P', 'mm', array(100,150));
+
+
+ + + diff --git a/application/third_party/fpdf/doc/acceptpagebreak.htm b/application/third_party/fpdf/doc/acceptpagebreak.htm new file mode 100644 index 0000000..640d1e0 --- /dev/null +++ b/application/third_party/fpdf/doc/acceptpagebreak.htm @@ -0,0 +1,63 @@ + + + + +AcceptPageBreak + + + +

AcceptPageBreak

+boolean AcceptPageBreak() +

Description

+Whenever a page break condition is met, the method is called, and the break is issued or not +depending on the returned value. The default implementation returns a value according to the +mode selected by SetAutoPageBreak(). +
+This method is called automatically and should not be called directly by the application. +

Example

+The method is overriden in an inherited class in order to obtain a 3 column layout: +
+
class PDF extends FPDF
+{
+    protected $col = 0;
+
+    function SetCol($col)
+    {
+        // Move position to a column
+        $this->col = $col;
+        $x = 10 + $col*65;
+        $this->SetLeftMargin($x);
+        $this->SetX($x);
+    }
+
+    function AcceptPageBreak()
+    {
+        if($this->col<2)
+        {
+            // Go to next column
+            $this->SetCol($this->col+1);
+            $this->SetY(10);
+            return false;
+        }
+        else
+        {
+            // Go back to first column and issue page break
+            $this->SetCol(0);
+            return true;
+        }
+    }
+}
+
+$pdf = new PDF();
+$pdf->AddPage();
+$pdf->SetFont('Arial', '', 12);
+for($i=1;$i<=300;$i++)
+    $pdf->Cell(0, 5, "Line $i", 0, 1);
+$pdf->Output();
+
+

See also

+SetAutoPageBreak +
+ + + diff --git a/application/third_party/fpdf/doc/addfont.htm b/application/third_party/fpdf/doc/addfont.htm new file mode 100644 index 0000000..1ce0505 --- /dev/null +++ b/application/third_party/fpdf/doc/addfont.htm @@ -0,0 +1,67 @@ + + + + +AddFont + + + +

AddFont

+AddFont(string family [, string style [, string file [, string dir]]]) +

Description

+Imports a TrueType, OpenType or Type1 font and makes it available. It is necessary to generate a font +definition file first with the MakeFont utility. +
+
+The definition file (and the font file itself in case of embedding) must be present in: +
    +
  • The directory indicated by the 4th parameter (if that parameter is set)
  • +
  • The directory indicated by the FPDF_FONTPATH constant (if that constant is defined)
  • +
  • The font directory located in the same directory as fpdf.php
  • +
+If the file is not found, the error "Could not include font definition file" is raised. +

Parameters

+
+
family
+
+Font family. The name can be chosen arbitrarily. If it is a standard family name, it will +override the corresponding font. +
+
style
+
+Font style. Possible values are (case insensitive): +
    +
  • empty string: regular
  • +
  • B: bold
  • +
  • I: italic
  • +
  • BI or IB: bold italic
  • +
+The default value is regular. +
+
file
+
+The name of the font definition file. +
+By default, it is built from the family and style, in lower case with no space. +
+
dir
+
+The directory where to load the definition file. +
+If not specified, the default directory will be used. +
+
+

Example

+
+
$pdf->AddFont('Comic', 'I');
+
+is equivalent to: +
+
$pdf->AddFont('Comic', 'I', 'comici.php');
+
+

See also

+SetFont +
+ + + diff --git a/application/third_party/fpdf/doc/addlink.htm b/application/third_party/fpdf/doc/addlink.htm new file mode 100644 index 0000000..0e8586a --- /dev/null +++ b/application/third_party/fpdf/doc/addlink.htm @@ -0,0 +1,26 @@ + + + + +AddLink + + + +

AddLink

+int AddLink() +

Description

+Creates a new internal link and returns its identifier. An internal link is a clickable area +which directs to another place within the document. +
+The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is +defined with SetLink(). +

See also

+Cell, +Write, +Image, +Link, +SetLink +
+ + + diff --git a/application/third_party/fpdf/doc/addpage.htm b/application/third_party/fpdf/doc/addpage.htm new file mode 100644 index 0000000..f75bc83 --- /dev/null +++ b/application/third_party/fpdf/doc/addpage.htm @@ -0,0 +1,61 @@ + + + + +AddPage + + + +

AddPage

+AddPage([string orientation [, mixed size [, int rotation]]]) +

Description

+Adds a new page to the document. If a page is already present, the Footer() method is called +first to output the footer. Then the page is added, the current position set to the top-left +corner according to the left and top margins, and Header() is called to display the header. +
+The font which was set before calling is automatically restored. There is no need to call +SetFont() again if you want to continue with the same font. The same is true for colors and +line width. +
+The origin of the coordinate system is at the top-left corner and increasing ordinates go +downwards. +

Parameters

+
+
orientation
+
+Page orientation. Possible values are (case insensitive): +
    +
  • P or Portrait
  • +
  • L or Landscape
  • +
+The default value is the one passed to the constructor. +
+
size
+
+Page size. It can be either one of the following values (case insensitive): +
    +
  • A3
  • +
  • A4
  • +
  • A5
  • +
  • Letter
  • +
  • Legal
  • +
+or an array containing the width and the height (expressed in user unit).
+
+The default value is the one passed to the constructor. +
+
rotation
+
+Angle by which to rotate the page. It must be a multiple of 90; positive values +mean clockwise rotation. The default value is 0. +
+
+

See also

+__construct, +Header, +Footer, +SetMargins +
+ + + diff --git a/application/third_party/fpdf/doc/aliasnbpages.htm b/application/third_party/fpdf/doc/aliasnbpages.htm new file mode 100644 index 0000000..7afa385 --- /dev/null +++ b/application/third_party/fpdf/doc/aliasnbpages.htm @@ -0,0 +1,45 @@ + + + + +AliasNbPages + + + +

AliasNbPages

+AliasNbPages([string alias]) +

Description

+Defines an alias for the total number of pages. It will be substituted as the document is +closed. +

Parameters

+
+
alias
+
+The alias. Default value: {nb}. +
+
+

Example

+
+
class PDF extends FPDF
+{
+    function Footer()
+    {
+        // Go to 1.5 cm from bottom
+        $this->SetY(-15);
+        // Select Arial italic 8
+        $this->SetFont('Arial', 'I', 8);
+        // Print current and total page numbers
+        $this->Cell(0, 10, 'Page '.$this->PageNo().'/{nb}', 0, 0, 'C');
+    }
+}
+
+$pdf = new PDF();
+$pdf->AliasNbPages();
+
+

See also

+PageNo, +Footer +
+ + + diff --git a/application/third_party/fpdf/doc/cell.htm b/application/third_party/fpdf/doc/cell.htm new file mode 100644 index 0000000..c2d9d1b --- /dev/null +++ b/application/third_party/fpdf/doc/cell.htm @@ -0,0 +1,104 @@ + + + + +Cell + + + +

Cell

+Cell(float w [, float h [, string txt [, mixed border [, int ln [, string align [, boolean fill [, mixed link]]]]]]]) +

Description

+Prints a cell (rectangular area) with optional borders, background color and character string. +The upper-left corner of the cell corresponds to the current position. The text can be aligned +or centered. After the call, the current position moves to the right or to the next line. It is +possible to put a link on the text. +
+If automatic page breaking is enabled and the cell goes beyond the limit, a page break is +done before outputting. +

Parameters

+
+
w
+
+Cell width. If 0, the cell extends up to the right margin. +
+
h
+
+Cell height. +Default value: 0. +
+
txt
+
+String to print. +Default value: empty string. +
+
border
+
+Indicates if borders must be drawn around the cell. The value can be either a number: +
    +
  • 0: no border
  • +
  • 1: frame
  • +
+or a string containing some or all of the following characters (in any order): +
    +
  • L: left
  • +
  • T: top
  • +
  • R: right
  • +
  • B: bottom
  • +
+Default value: 0. +
+
ln
+
+Indicates where the current position should go after the call. Possible values are: +
    +
  • 0: to the right
  • +
  • 1: to the beginning of the next line
  • +
  • 2: below
  • +
+Putting 1 is equivalent to putting 0 and calling Ln() just after. +Default value: 0. +
+
align
+
+Allows to center or align the text. Possible values are: +
    +
  • L or empty string: left align (default value)
  • +
  • C: center
  • +
  • R: right align
  • +
+
+
fill
+
+Indicates if the cell background must be painted (true) or transparent (false). +Default value: false. +
+
link
+
+URL or identifier returned by AddLink(). +
+
+

Example

+
+
// Set font
+$pdf->SetFont('Arial', 'B', 16);
+// Move to 8 cm to the right
+$pdf->Cell(80);
+// Centered text in a framed 20*10 mm cell and line break
+$pdf->Cell(20, 10, 'Title', 1, 1, 'C');
+
+

See also

+SetFont, +SetDrawColor, +SetFillColor, +SetTextColor, +SetLineWidth, +AddLink, +Ln, +MultiCell, +Write, +SetAutoPageBreak +
+ + + diff --git a/application/third_party/fpdf/doc/close.htm b/application/third_party/fpdf/doc/close.htm new file mode 100644 index 0000000..596cfe4 --- /dev/null +++ b/application/third_party/fpdf/doc/close.htm @@ -0,0 +1,21 @@ + + + + +Close + + + +

Close

+Close() +

Description

+Terminates the PDF document. It is not necessary to call this method explicitly because Output() +does it automatically. +
+If the document contains no page, AddPage() is called to prevent from getting an invalid document. +

See also

+Output +
+ + + diff --git a/application/third_party/fpdf/doc/error.htm b/application/third_party/fpdf/doc/error.htm new file mode 100644 index 0000000..16bcde9 --- /dev/null +++ b/application/third_party/fpdf/doc/error.htm @@ -0,0 +1,26 @@ + + + + +Error + + + +

Error

+Error(string msg) +

Description

+This method is automatically called in case of a fatal error; it simply throws an exception +with the provided message.
+An inherited class may override it to customize the error handling but the method should +never return, otherwise the resulting document would probably be invalid. +

Parameters

+
+
msg
+
+The error message. +
+
+
+ + + diff --git a/application/third_party/fpdf/doc/footer.htm b/application/third_party/fpdf/doc/footer.htm new file mode 100644 index 0000000..40bf6b1 --- /dev/null +++ b/application/third_party/fpdf/doc/footer.htm @@ -0,0 +1,35 @@ + + + + +Footer + + + +

Footer

+Footer() +

Description

+This method is used to render the page footer. It is automatically called by AddPage() and +Close() and should not be called directly by the application. The implementation in FPDF is +empty, so you have to subclass it and override the method if you want a specific processing. +

Example

+
+
class PDF extends FPDF
+{
+    function Footer()
+    {
+        // Go to 1.5 cm from bottom
+        $this->SetY(-15);
+        // Select Arial italic 8
+        $this->SetFont('Arial', 'I', 8);
+        // Print centered page number
+        $this->Cell(0, 10, 'Page '.$this->PageNo(), 0, 0, 'C');
+    }
+}
+
+

See also

+Header +
+ + + diff --git a/application/third_party/fpdf/doc/getpageheight.htm b/application/third_party/fpdf/doc/getpageheight.htm new file mode 100644 index 0000000..2727c0e --- /dev/null +++ b/application/third_party/fpdf/doc/getpageheight.htm @@ -0,0 +1,18 @@ + + + + +GetPageHeight + + + +

GetPageHeight

+float GetPageHeight() +

Description

+Returns the current page height. +

See also

+GetPageWidth +
+ + + diff --git a/application/third_party/fpdf/doc/getpagewidth.htm b/application/third_party/fpdf/doc/getpagewidth.htm new file mode 100644 index 0000000..3343389 --- /dev/null +++ b/application/third_party/fpdf/doc/getpagewidth.htm @@ -0,0 +1,18 @@ + + + + +GetPageWidth + + + +

GetPageWidth

+float GetPageWidth() +

Description

+Returns the current page width. +

See also

+GetPageHeight +
+ + + diff --git a/application/third_party/fpdf/doc/getstringwidth.htm b/application/third_party/fpdf/doc/getstringwidth.htm new file mode 100644 index 0000000..4aba124 --- /dev/null +++ b/application/third_party/fpdf/doc/getstringwidth.htm @@ -0,0 +1,23 @@ + + + + +GetStringWidth + + + +

GetStringWidth

+float GetStringWidth(string s) +

Description

+Returns the length of a string in user unit. A font must be selected. +

Parameters

+
+
s
+
+The string whose length is to be computed. +
+
+
+ + + diff --git a/application/third_party/fpdf/doc/getx.htm b/application/third_party/fpdf/doc/getx.htm new file mode 100644 index 0000000..91bed5d --- /dev/null +++ b/application/third_party/fpdf/doc/getx.htm @@ -0,0 +1,20 @@ + + + + +GetX + + + +

GetX

+float GetX() +

Description

+Returns the abscissa of the current position. +

See also

+SetX, +GetY, +SetY +
+ + + diff --git a/application/third_party/fpdf/doc/gety.htm b/application/third_party/fpdf/doc/gety.htm new file mode 100644 index 0000000..4e28883 --- /dev/null +++ b/application/third_party/fpdf/doc/gety.htm @@ -0,0 +1,20 @@ + + + + +GetY + + + +

GetY

+float GetY() +

Description

+Returns the ordinate of the current position. +

See also

+SetY, +GetX, +SetX +
+ + + diff --git a/application/third_party/fpdf/doc/header.htm b/application/third_party/fpdf/doc/header.htm new file mode 100644 index 0000000..851afde --- /dev/null +++ b/application/third_party/fpdf/doc/header.htm @@ -0,0 +1,37 @@ + + + + +Header + + + +

Header

+Header() +

Description

+This method is used to render the page header. It is automatically called by AddPage() and +should not be called directly by the application. The implementation in FPDF is empty, so +you have to subclass it and override the method if you want a specific processing. +

Example

+
+
class PDF extends FPDF
+{
+    function Header()
+    {
+        // Select Arial bold 15
+        $this->SetFont('Arial', 'B', 15);
+        // Move to the right
+        $this->Cell(80);
+        // Framed title
+        $this->Cell(30, 10, 'Title', 1, 0, 'C');
+        // Line break
+        $this->Ln(20);
+    }
+}
+
+

See also

+Footer +
+ + + diff --git a/application/third_party/fpdf/doc/image.htm b/application/third_party/fpdf/doc/image.htm new file mode 100644 index 0000000..b2296ab --- /dev/null +++ b/application/third_party/fpdf/doc/image.htm @@ -0,0 +1,99 @@ + + + + +Image + + + +

Image

+Image(string file [, float x [, float y [, float w [, float h [, string type [, mixed link]]]]]]) +

Description

+Puts an image. The size it will take on the page can be specified in different ways: +
    +
  • explicit width and height (expressed in user unit or dpi)
  • +
  • one explicit dimension, the other being calculated automatically in order to keep the original proportions
  • +
  • no explicit dimension, in which case the image is put at 96 dpi
  • +
+Supported formats are JPEG, PNG and GIF. The GD extension is required for GIF. +
+
+For JPEGs, all flavors are allowed: +
    +
  • gray scales
  • +
  • true colors (24 bits)
  • +
  • CMYK (32 bits)
  • +
+For PNGs, are allowed: +
    +
  • gray scales on at most 8 bits (256 levels)
  • +
  • indexed colors
  • +
  • true colors (24 bits)
  • +
+For GIFs: in case of an animated GIF, only the first frame is displayed.
+
+Transparency is supported.
+
+The format can be specified explicitly or inferred from the file extension.
+
+It is possible to put a link on the image.
+
+Remark: if an image is used several times, only one copy is embedded in the file. +

Parameters

+
+
file
+
+Path or URL of the image. +
+
x
+
+Abscissa of the upper-left corner. If not specified or equal to null, the current abscissa +is used. +
+
y
+
+Ordinate of the upper-left corner. If not specified or equal to null, the current ordinate +is used; moreover, a page break is triggered first if necessary (in case automatic page breaking is enabled) +and, after the call, the current ordinate is moved to the bottom of the image. +
+
w
+
+Width of the image in the page. There are three cases: +
    +
  • If the value is positive, it represents the width in user unit
  • +
  • If the value is negative, the absolute value represents the horizontal resolution in dpi
  • +
  • If the value is not specified or equal to zero, it is automatically calculated
  • +
+
+
h
+
+Height of the image in the page. There are three cases: +
    +
  • If the value is positive, it represents the height in user unit
  • +
  • If the value is negative, the absolute value represents the vertical resolution in dpi
  • +
  • If the value is not specified or equal to zero, it is automatically calculated
  • +
+
+
type
+
+Image format. Possible values are (case insensitive): JPG, JPEG, PNG and GIF. +If not specified, the type is inferred from the file extension. +
+
link
+
+URL or identifier returned by AddLink(). +
+
+

Example

+
+
// Insert a logo in the top-left corner at 300 dpi
+$pdf->Image('logo.png', 10, 10, -300);
+// Insert a dynamic image from a URL
+$pdf->Image('http://chart.googleapis.com/chart?cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World', 60, 30, 90, 0, 'PNG');
+
+

See also

+AddLink +
+ + + diff --git a/application/third_party/fpdf/doc/index.htm b/application/third_party/fpdf/doc/index.htm new file mode 100644 index 0000000..97f4769 --- /dev/null +++ b/application/third_party/fpdf/doc/index.htm @@ -0,0 +1,59 @@ + + + + +Documentation + + + +

Documentation

+__construct - constructor
+AcceptPageBreak - accept or not automatic page break
+AddFont - add a new font
+AddLink - create an internal link
+AddPage - add a new page
+AliasNbPages - define an alias for number of pages
+Cell - print a cell
+Close - terminate the document
+Error - fatal error
+Footer - page footer
+GetPageHeight - get current page height
+GetPageWidth - get current page width
+GetStringWidth - compute string length
+GetX - get current x position
+GetY - get current y position
+Header - page header
+Image - output an image
+Line - draw a line
+Link - put a link
+Ln - line break
+MultiCell - print text with line breaks
+Output - save or send the document
+PageNo - page number
+Rect - draw a rectangle
+SetAuthor - set the document author
+SetAutoPageBreak - set the automatic page breaking mode
+SetCompression - turn compression on or off
+SetCreator - set document creator
+SetDisplayMode - set display mode
+SetDrawColor - set drawing color
+SetFillColor - set filling color
+SetFont - set font
+SetFontSize - set font size
+SetKeywords - associate keywords with document
+SetLeftMargin - set left margin
+SetLineWidth - set line width
+SetLink - set internal link destination
+SetMargins - set margins
+SetRightMargin - set right margin
+SetSubject - set document subject
+SetTextColor - set text color
+SetTitle - set document title
+SetTopMargin - set top margin
+SetX - set current x position
+SetXY - set current x and y positions
+SetY - set current y position and optionally reset x
+Text - print a string
+Write - print flowing text
+ + diff --git a/application/third_party/fpdf/doc/line.htm b/application/third_party/fpdf/doc/line.htm new file mode 100644 index 0000000..5cb0607 --- /dev/null +++ b/application/third_party/fpdf/doc/line.htm @@ -0,0 +1,38 @@ + + + + +Line + + + +

Line

+Line(float x1, float y1, float x2, float y2) +

Description

+Draws a line between two points. +

Parameters

+
+
x1
+
+Abscissa of first point. +
+
y1
+
+Ordinate of first point. +
+
x2
+
+Abscissa of second point. +
+
y2
+
+Ordinate of second point. +
+
+

See also

+SetLineWidth, +SetDrawColor +
+ + + diff --git a/application/third_party/fpdf/doc/link.htm b/application/third_party/fpdf/doc/link.htm new file mode 100644 index 0000000..46ba0f2 --- /dev/null +++ b/application/third_party/fpdf/doc/link.htm @@ -0,0 +1,46 @@ + + + + +Link + + + +

Link

+Link(float x, float y, float w, float h, mixed link) +

Description

+Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), +Write() or Image(), but this method can be useful for instance to define a clickable area inside +an image. +

Parameters

+
+
x
+
+Abscissa of the upper-left corner of the rectangle. +
+
y
+
+Ordinate of the upper-left corner of the rectangle. +
+
w
+
+Width of the rectangle. +
+
h
+
+Height of the rectangle. +
+
link
+
+URL or identifier returned by AddLink(). +
+
+

See also

+AddLink, +Cell, +Write, +Image +
+ + + diff --git a/application/third_party/fpdf/doc/ln.htm b/application/third_party/fpdf/doc/ln.htm new file mode 100644 index 0000000..871e507 --- /dev/null +++ b/application/third_party/fpdf/doc/ln.htm @@ -0,0 +1,28 @@ + + + + +Ln + + + +

Ln

+Ln([float h]) +

Description

+Performs a line break. The current abscissa goes back to the left margin and the ordinate +increases by the amount passed in parameter. +

Parameters

+
+
h
+
+The height of the break. +
+By default, the value equals the height of the last printed cell. +
+
+

See also

+Cell +
+ + + diff --git a/application/third_party/fpdf/doc/multicell.htm b/application/third_party/fpdf/doc/multicell.htm new file mode 100644 index 0000000..5361670 --- /dev/null +++ b/application/third_party/fpdf/doc/multicell.htm @@ -0,0 +1,76 @@ + + + + +MultiCell + + + +

MultiCell

+MultiCell(float w, float h, string txt [, mixed border [, string align [, boolean fill]]]) +

Description

+This method allows printing text with line breaks. They can be automatic (as soon as the +text reaches the right border of the cell) or explicit (via the \n character). As many cells +as necessary are output, one below the other. +
+Text can be aligned, centered or justified. The cell block can be framed and the background +painted. +

Parameters

+
+
w
+
+Width of cells. If 0, they extend up to the right margin of the page. +
+
h
+
+Height of cells. +
+
txt
+
+String to print. +
+
border
+
+Indicates if borders must be drawn around the cell block. The value can be either a number: +
    +
  • 0: no border
  • +
  • 1: frame
  • +
+or a string containing some or all of the following characters (in any order): +
    +
  • L: left
  • +
  • T: top
  • +
  • R: right
  • +
  • B: bottom
  • +
+Default value: 0. +
+
align
+
+Sets the text alignment. Possible values are: +
    +
  • L: left alignment
  • +
  • C: center
  • +
  • R: right alignment
  • +
  • J: justification (default value)
  • +
+
+
fill
+
+Indicates if the cell background must be painted (true) or transparent (false). +Default value: false. +
+
+

See also

+SetFont, +SetDrawColor, +SetFillColor, +SetTextColor, +SetLineWidth, +Cell, +Write, +SetAutoPageBreak +
+ + + diff --git a/application/third_party/fpdf/doc/output.htm b/application/third_party/fpdf/doc/output.htm new file mode 100644 index 0000000..7204bc2 --- /dev/null +++ b/application/third_party/fpdf/doc/output.htm @@ -0,0 +1,55 @@ + + + + +Output + + + +

Output

+string Output([string dest [, string name [, boolean isUTF8]]]) +

Description

+Send the document to a given destination: browser, file or string. In the case of a browser, the +PDF viewer may be used or a download may be forced. +
+The method first calls Close() if necessary to terminate the document. +

Parameters

+
+
dest
+
+Destination where to send the document. It can be one of the following: +
    +
  • I: send the file inline to the browser. The PDF viewer is used if available.
  • +
  • D: send to the browser and force a file download with the name given by name.
  • +
  • F: save to a local file with the name given by name (may include a path).
  • +
  • S: return the document as a string.
  • +
+The default value is I. +
+
name
+
+The name of the file. It is ignored in case of destination S.
+The default value is doc.pdf. +
+
isUTF8
+
+Indicates if name is encoded in ISO-8859-1 (false) or UTF-8 (true). +Only used for destinations I and D.
+The default value is false. +
+
+

Example

+Save the document to a local directory: +
+
$pdf->Output('F', 'reports/report.pdf');
+
+Force a download: +
+
$pdf->Output('D', 'report.pdf');
+
+

See also

+Close +
+ + + diff --git a/application/third_party/fpdf/doc/pageno.htm b/application/third_party/fpdf/doc/pageno.htm new file mode 100644 index 0000000..cfbea9f --- /dev/null +++ b/application/third_party/fpdf/doc/pageno.htm @@ -0,0 +1,18 @@ + + + + +PageNo + + + +

PageNo

+int PageNo() +

Description

+Returns the current page number. +

See also

+AliasNbPages +
+ + + diff --git a/application/third_party/fpdf/doc/rect.htm b/application/third_party/fpdf/doc/rect.htm new file mode 100644 index 0000000..7f2e5b8 --- /dev/null +++ b/application/third_party/fpdf/doc/rect.htm @@ -0,0 +1,48 @@ + + + + +Rect + + + +

Rect

+Rect(float x, float y, float w, float h [, string style]) +

Description

+Outputs a rectangle. It can be drawn (border only), filled (with no border) or both. +

Parameters

+
+
x
+
+Abscissa of upper-left corner. +
+
y
+
+Ordinate of upper-left corner. +
+
w
+
+Width. +
+
h
+
+Height. +
+
style
+
+Style of rendering. Possible values are: +
    +
  • D or empty string: draw. This is the default value.
  • +
  • F: fill
  • +
  • DF or FD: draw and fill
  • +
+
+
+

See also

+SetLineWidth, +SetDrawColor, +SetFillColor +
+ + + diff --git a/application/third_party/fpdf/doc/setauthor.htm b/application/third_party/fpdf/doc/setauthor.htm new file mode 100644 index 0000000..3406d1b --- /dev/null +++ b/application/third_party/fpdf/doc/setauthor.htm @@ -0,0 +1,33 @@ + + + + +SetAuthor + + + +

SetAuthor

+SetAuthor(string author [, boolean isUTF8]) +

Description

+Defines the author of the document. +

Parameters

+
+
author
+
+The name of the author. +
+
isUTF8
+
+Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true).
+Default value: false. +
+
+

See also

+SetCreator, +SetKeywords, +SetSubject, +SetTitle +
+ + + diff --git a/application/third_party/fpdf/doc/setautopagebreak.htm b/application/third_party/fpdf/doc/setautopagebreak.htm new file mode 100644 index 0000000..ae598d0 --- /dev/null +++ b/application/third_party/fpdf/doc/setautopagebreak.htm @@ -0,0 +1,33 @@ + + + + +SetAutoPageBreak + + + +

SetAutoPageBreak

+SetAutoPageBreak(boolean auto [, float margin]) +

Description

+Enables or disables the automatic page breaking mode. When enabling, the second parameter is +the distance from the bottom of the page that defines the triggering limit. By default, the +mode is on and the margin is 2 cm. +

Parameters

+
+
auto
+
+Boolean indicating if mode should be on or off. +
+
margin
+
+Distance from the bottom of the page. +
+
+

See also

+Cell, +MultiCell, +AcceptPageBreak +
+ + + diff --git a/application/third_party/fpdf/doc/setcompression.htm b/application/third_party/fpdf/doc/setcompression.htm new file mode 100644 index 0000000..d60c905 --- /dev/null +++ b/application/third_party/fpdf/doc/setcompression.htm @@ -0,0 +1,31 @@ + + + + +SetCompression + + + +

SetCompression

+SetCompression(boolean compress) +

Description

+Activates or deactivates page compression. When activated, the internal representation of +each page is compressed, which leads to a compression ratio of about 2 for the resulting +document. +
+Compression is on by default. +
+
+Note: the Zlib extension is required for this feature. If not present, compression +will be turned off. +

Parameters

+
+
compress
+
+Boolean indicating if compression must be enabled. +
+
+
+ + + diff --git a/application/third_party/fpdf/doc/setcreator.htm b/application/third_party/fpdf/doc/setcreator.htm new file mode 100644 index 0000000..d19ddde --- /dev/null +++ b/application/third_party/fpdf/doc/setcreator.htm @@ -0,0 +1,34 @@ + + + + +SetCreator + + + +

SetCreator

+SetCreator(string creator [, boolean isUTF8]) +

Description

+Defines the creator of the document. This is typically the name of the application that +generates the PDF. +

Parameters

+
+
creator
+
+The name of the creator. +
+
isUTF8
+
+Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true).
+Default value: false. +
+
+

See also

+SetAuthor, +SetKeywords, +SetSubject, +SetTitle +
+ + + diff --git a/application/third_party/fpdf/doc/setdisplaymode.htm b/application/third_party/fpdf/doc/setdisplaymode.htm new file mode 100644 index 0000000..5861581 --- /dev/null +++ b/application/third_party/fpdf/doc/setdisplaymode.htm @@ -0,0 +1,45 @@ + + + + +SetDisplayMode + + + +

SetDisplayMode

+SetDisplayMode(mixed zoom [, string layout]) +

Description

+Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be +displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a +specific zooming factor or use viewer default (configured in the Preferences menu of Adobe Reader). +The page layout can be specified too: single at once, continuous display, two columns or viewer +default. +

Parameters

+
+
zoom
+
+The zoom to use. It can be one of the following string values: +
    +
  • fullpage: displays the entire page on screen
  • +
  • fullwidth: uses maximum width of window
  • +
  • real: uses real size (equivalent to 100% zoom)
  • +
  • default: uses viewer default mode
  • +
+or a number indicating the zooming factor to use. +
+
layout
+
+The page layout. Possible values are: +
    +
  • single: displays one page at once
  • +
  • continuous: displays pages continuously
  • +
  • two: displays two pages on two columns
  • +
  • default: uses viewer default mode
  • +
+Default value is default. +
+
+
+ + + diff --git a/application/third_party/fpdf/doc/setdrawcolor.htm b/application/third_party/fpdf/doc/setdrawcolor.htm new file mode 100644 index 0000000..42ce061 --- /dev/null +++ b/application/third_party/fpdf/doc/setdrawcolor.htm @@ -0,0 +1,41 @@ + + + + +SetDrawColor + + + +

SetDrawColor

+SetDrawColor(int r [, int g, int b]) +

Description

+Defines the color used for all drawing operations (lines, rectangles and cell borders). It +can be expressed in RGB components or gray scale. The method can be called before the first +page is created and the value is retained from page to page. +

Parameters

+
+
r
+
+If g et b are given, red component; if not, indicates the gray level. +Value between 0 and 255. +
+
g
+
+Green component (between 0 and 255). +
+
b
+
+Blue component (between 0 and 255). +
+
+

See also

+SetFillColor, +SetTextColor, +Line, +Rect, +Cell, +MultiCell +
+ + + diff --git a/application/third_party/fpdf/doc/setfillcolor.htm b/application/third_party/fpdf/doc/setfillcolor.htm new file mode 100644 index 0000000..df519a5 --- /dev/null +++ b/application/third_party/fpdf/doc/setfillcolor.htm @@ -0,0 +1,40 @@ + + + + +SetFillColor + + + +

SetFillColor

+SetFillColor(int r [, int g, int b]) +

Description

+Defines the color used for all filling operations (filled rectangles and cell backgrounds). +It can be expressed in RGB components or gray scale. The method can be called before the first +page is created and the value is retained from page to page. +

Parameters

+
+
r
+
+If g and b are given, red component; if not, indicates the gray level. +Value between 0 and 255. +
+
g
+
+Green component (between 0 and 255). +
+
b
+
+Blue component (between 0 and 255). +
+
+

See also

+SetDrawColor, +SetTextColor, +Rect, +Cell, +MultiCell +
+ + + diff --git a/application/third_party/fpdf/doc/setfont.htm b/application/third_party/fpdf/doc/setfont.htm new file mode 100644 index 0000000..d7fb73c --- /dev/null +++ b/application/third_party/fpdf/doc/setfont.htm @@ -0,0 +1,78 @@ + + + + +SetFont + + + +

SetFont

+SetFont(string family [, string style [, float size]]) +

Description

+Sets the font used to print character strings. It is mandatory to call this method at least once before printing text. +
+
+The font can be either a standard one or a font added by the AddFont() method. Standard fonts +use the Windows encoding cp1252 (Western Europe). +
+
+The method can be called before the first page is created and the font is kept from page to page. +
+
+If you just wish to change the current font size, it is simpler to call SetFontSize(). +

Parameters

+
+
family
+
+Family font. It can be either a name defined by AddFont() or one of the standard families (case +insensitive): +
    +
  • Courier (fixed-width)
  • +
  • Helvetica or Arial (synonymous; sans serif)
  • +
  • Times (serif)
  • +
  • Symbol (symbolic)
  • +
  • ZapfDingbats (symbolic)
  • +
+It is also possible to pass an empty string. In that case, the current family is kept. +
+
style
+
+Font style. Possible values are (case insensitive): +
    +
  • empty string: regular
  • +
  • B: bold
  • +
  • I: italic
  • +
  • U: underline
  • +
+or any combination. The default value is regular. +Bold and italic styles do not apply to Symbol and ZapfDingbats. +
+
size
+
+Font size in points. +
+The default value is the current size. If no size has been specified since the beginning of +the document, the value is 12. +
+
+

Example

+
+
// Times regular 12
+$pdf->SetFont('Times');
+// Arial bold 14
+$pdf->SetFont('Arial', 'B', 14);
+// Removes bold
+$pdf->SetFont('');
+// Times bold, italic and underlined 14
+$pdf->SetFont('Times', 'BIU');
+
+

See also

+AddFont, +SetFontSize, +Cell, +MultiCell, +Write +
+ + + diff --git a/application/third_party/fpdf/doc/setfontsize.htm b/application/third_party/fpdf/doc/setfontsize.htm new file mode 100644 index 0000000..a5badd4 --- /dev/null +++ b/application/third_party/fpdf/doc/setfontsize.htm @@ -0,0 +1,25 @@ + + + + +SetFontSize + + + +

SetFontSize

+SetFontSize(float size) +

Description

+Defines the size of the current font. +

Parameters

+
+
size
+
+The size (in points). +
+
+

See also

+SetFont +
+ + + diff --git a/application/third_party/fpdf/doc/setkeywords.htm b/application/third_party/fpdf/doc/setkeywords.htm new file mode 100644 index 0000000..647f12f --- /dev/null +++ b/application/third_party/fpdf/doc/setkeywords.htm @@ -0,0 +1,33 @@ + + + + +SetKeywords + + + +

SetKeywords

+SetKeywords(string keywords [, boolean isUTF8]) +

Description

+Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'. +

Parameters

+
+
keywords
+
+The list of keywords. +
+
isUTF8
+
+Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true).
+Default value: false. +
+
+

See also

+SetAuthor, +SetCreator, +SetSubject, +SetTitle +
+ + + diff --git a/application/third_party/fpdf/doc/setleftmargin.htm b/application/third_party/fpdf/doc/setleftmargin.htm new file mode 100644 index 0000000..d8d2f3b --- /dev/null +++ b/application/third_party/fpdf/doc/setleftmargin.htm @@ -0,0 +1,30 @@ + + + + +SetLeftMargin + + + +

SetLeftMargin

+SetLeftMargin(float margin) +

Description

+Defines the left margin. The method can be called before creating the first page. +
+If the current abscissa gets out of page, it is brought back to the margin. +

Parameters

+
+
margin
+
+The margin. +
+
+

See also

+SetTopMargin, +SetRightMargin, +SetAutoPageBreak, +SetMargins +
+ + + diff --git a/application/third_party/fpdf/doc/setlinewidth.htm b/application/third_party/fpdf/doc/setlinewidth.htm new file mode 100644 index 0000000..92b18f8 --- /dev/null +++ b/application/third_party/fpdf/doc/setlinewidth.htm @@ -0,0 +1,29 @@ + + + + +SetLineWidth + + + +

SetLineWidth

+SetLineWidth(float width) +

Description

+Defines the line width. By default, the value equals 0.2 mm. The method can be called before +the first page is created and the value is retained from page to page. +

Parameters

+
+
width
+
+The width. +
+
+

See also

+Line, +Rect, +Cell, +MultiCell +
+ + + diff --git a/application/third_party/fpdf/doc/setlink.htm b/application/third_party/fpdf/doc/setlink.htm new file mode 100644 index 0000000..443517b --- /dev/null +++ b/application/third_party/fpdf/doc/setlink.htm @@ -0,0 +1,34 @@ + + + + +SetLink + + + +

SetLink

+SetLink(int link [, float y [, int page]]) +

Description

+Defines the page and position a link points to. +

Parameters

+
+
link
+
+The link identifier returned by AddLink(). +
+
y
+
+Ordinate of target position; -1 indicates the current position. +The default value is 0 (top of page). +
+
page
+
+Number of target page; -1 indicates the current page. This is the default value. +
+
+

See also

+AddLink +
+ + + diff --git a/application/third_party/fpdf/doc/setmargins.htm b/application/third_party/fpdf/doc/setmargins.htm new file mode 100644 index 0000000..dc9b9ee --- /dev/null +++ b/application/third_party/fpdf/doc/setmargins.htm @@ -0,0 +1,37 @@ + + + + +SetMargins + + + +

SetMargins

+SetMargins(float left, float top [, float right]) +

Description

+Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change +them. +

Parameters

+
+
left
+
+Left margin. +
+
top
+
+Top margin. +
+
right
+
+Right margin. Default value is the left one. +
+
+

See also

+SetLeftMargin, +SetTopMargin, +SetRightMargin, +SetAutoPageBreak +
+ + + diff --git a/application/third_party/fpdf/doc/setrightmargin.htm b/application/third_party/fpdf/doc/setrightmargin.htm new file mode 100644 index 0000000..1c45166 --- /dev/null +++ b/application/third_party/fpdf/doc/setrightmargin.htm @@ -0,0 +1,28 @@ + + + + +SetRightMargin + + + +

SetRightMargin

+SetRightMargin(float margin) +

Description

+Defines the right margin. The method can be called before creating the first page. +

Parameters

+
+
margin
+
+The margin. +
+
+

See also

+SetLeftMargin, +SetTopMargin, +SetAutoPageBreak, +SetMargins +
+ + + diff --git a/application/third_party/fpdf/doc/setsubject.htm b/application/third_party/fpdf/doc/setsubject.htm new file mode 100644 index 0000000..c4643f1 --- /dev/null +++ b/application/third_party/fpdf/doc/setsubject.htm @@ -0,0 +1,33 @@ + + + + +SetSubject + + + +

SetSubject

+SetSubject(string subject [, boolean isUTF8]) +

Description

+Defines the subject of the document. +

Parameters

+
+
subject
+
+The subject. +
+
isUTF8
+
+Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true).
+Default value: false. +
+
+

See also

+SetAuthor, +SetCreator, +SetKeywords, +SetTitle +
+ + + diff --git a/application/third_party/fpdf/doc/settextcolor.htm b/application/third_party/fpdf/doc/settextcolor.htm new file mode 100644 index 0000000..a7fbd97 --- /dev/null +++ b/application/third_party/fpdf/doc/settextcolor.htm @@ -0,0 +1,40 @@ + + + + +SetTextColor + + + +

SetTextColor

+SetTextColor(int r [, int g, int b]) +

Description

+Defines the color used for text. It can be expressed in RGB components or gray scale. The +method can be called before the first page is created and the value is retained from page to +page. +

Parameters

+
+
r
+
+If g et b are given, red component; if not, indicates the gray level. +Value between 0 and 255. +
+
g
+
+Green component (between 0 and 255). +
+
b
+
+Blue component (between 0 and 255). +
+
+

See also

+SetDrawColor, +SetFillColor, +Text, +Cell, +MultiCell +
+ + + diff --git a/application/third_party/fpdf/doc/settitle.htm b/application/third_party/fpdf/doc/settitle.htm new file mode 100644 index 0000000..b8b6925 --- /dev/null +++ b/application/third_party/fpdf/doc/settitle.htm @@ -0,0 +1,33 @@ + + + + +SetTitle + + + +

SetTitle

+SetTitle(string title [, boolean isUTF8]) +

Description

+Defines the title of the document. +

Parameters

+
+
title
+
+The title. +
+
isUTF8
+
+Indicates if the string is encoded in ISO-8859-1 (false) or UTF-8 (true).
+Default value: false. +
+
+

See also

+SetAuthor, +SetCreator, +SetKeywords, +SetSubject +
+ + + diff --git a/application/third_party/fpdf/doc/settopmargin.htm b/application/third_party/fpdf/doc/settopmargin.htm new file mode 100644 index 0000000..46a7952 --- /dev/null +++ b/application/third_party/fpdf/doc/settopmargin.htm @@ -0,0 +1,28 @@ + + + + +SetTopMargin + + + +

SetTopMargin

+SetTopMargin(float margin) +

Description

+Defines the top margin. The method can be called before creating the first page. +

Parameters

+
+
margin
+
+The margin. +
+
+

See also

+SetLeftMargin, +SetRightMargin, +SetAutoPageBreak, +SetMargins +
+ + + diff --git a/application/third_party/fpdf/doc/setx.htm b/application/third_party/fpdf/doc/setx.htm new file mode 100644 index 0000000..282898c --- /dev/null +++ b/application/third_party/fpdf/doc/setx.htm @@ -0,0 +1,29 @@ + + + + +SetX + + + +

SetX

+SetX(float x) +

Description

+Defines the abscissa of the current position. If the passed value is negative, it is relative +to the right of the page. +

Parameters

+
+
x
+
+The value of the abscissa. +
+
+

See also

+GetX, +GetY, +SetY, +SetXY +
+ + + diff --git a/application/third_party/fpdf/doc/setxy.htm b/application/third_party/fpdf/doc/setxy.htm new file mode 100644 index 0000000..36eee62 --- /dev/null +++ b/application/third_party/fpdf/doc/setxy.htm @@ -0,0 +1,31 @@ + + + + +SetXY + + + +

SetXY

+SetXY(float x, float y) +

Description

+Defines the abscissa and ordinate of the current position. If the passed values are negative, +they are relative respectively to the right and bottom of the page. +

Parameters

+
+
x
+
+The value of the abscissa. +
+
y
+
+The value of the ordinate. +
+
+

See also

+SetX, +SetY +
+ + + diff --git a/application/third_party/fpdf/doc/sety.htm b/application/third_party/fpdf/doc/sety.htm new file mode 100644 index 0000000..19a4451 --- /dev/null +++ b/application/third_party/fpdf/doc/sety.htm @@ -0,0 +1,33 @@ + + + + +SetY + + + +

SetY

+SetY(float y [, boolean resetX]) +

Description

+Sets the ordinate and optionally moves the current abscissa back to the left margin. If the value +is negative, it is relative to the bottom of the page. +

Parameters

+
+
y
+
+The value of the ordinate. +
+
resetX
+
+Whether to reset the abscissa. Default value: true. +
+
+

See also

+GetX, +GetY, +SetX, +SetXY +
+ + + diff --git a/application/third_party/fpdf/doc/text.htm b/application/third_party/fpdf/doc/text.htm new file mode 100644 index 0000000..11d5600 --- /dev/null +++ b/application/third_party/fpdf/doc/text.htm @@ -0,0 +1,39 @@ + + + + +Text + + + +

Text

+Text(float x, float y, string txt) +

Description

+Prints a character string. The origin is on the left of the first character, on the baseline. +This method allows to place a string precisely on the page, but it is usually easier to use +Cell(), MultiCell() or Write() which are the standard methods to print text. +

Parameters

+
+
x
+
+Abscissa of the origin. +
+
y
+
+Ordinate of the origin. +
+
txt
+
+String to print. +
+
+

See also

+SetFont, +SetTextColor, +Cell, +MultiCell, +Write +
+ + + diff --git a/application/third_party/fpdf/doc/write.htm b/application/third_party/fpdf/doc/write.htm new file mode 100644 index 0000000..9653540 --- /dev/null +++ b/application/third_party/fpdf/doc/write.htm @@ -0,0 +1,51 @@ + + + + +Write + + + +

Write

+Write(float h, string txt [, mixed link]) +

Description

+This method prints text from the current position. When the right margin is reached (or the \n +character is met) a line break occurs and text continues from the left margin. Upon method exit, +the current position is left just at the end of the text. +
+It is possible to put a link on the text. +

Parameters

+
+
h
+
+Line height. +
+
txt
+
+String to print. +
+
link
+
+URL or identifier returned by AddLink(). +
+
+

Example

+
+
// Begin with regular font
+$pdf->SetFont('Arial', '', 14);
+$pdf->Write(5, 'Visit ');
+// Then put a blue underlined link
+$pdf->SetTextColor(0, 0, 255);
+$pdf->SetFont('', 'U');
+$pdf->Write(5, 'www.fpdf.org', 'http://www.fpdf.org');
+
+

See also

+SetFont, +SetTextColor, +AddLink, +MultiCell, +SetAutoPageBreak +
+ + + diff --git a/application/third_party/fpdf/font/courier.php b/application/third_party/fpdf/font/courier.php new file mode 100644 index 0000000..67dbeda --- /dev/null +++ b/application/third_party/fpdf/font/courier.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/courierb.php b/application/third_party/fpdf/font/courierb.php new file mode 100644 index 0000000..62550a4 --- /dev/null +++ b/application/third_party/fpdf/font/courierb.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/courierbi.php b/application/third_party/fpdf/font/courierbi.php new file mode 100644 index 0000000..6a3ecc6 --- /dev/null +++ b/application/third_party/fpdf/font/courierbi.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/courieri.php b/application/third_party/fpdf/font/courieri.php new file mode 100644 index 0000000..b88e098 --- /dev/null +++ b/application/third_party/fpdf/font/courieri.php @@ -0,0 +1,10 @@ +array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/helvetica.php b/application/third_party/fpdf/font/helvetica.php new file mode 100644 index 0000000..2be3eca --- /dev/null +++ b/application/third_party/fpdf/font/helvetica.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/helveticab.php b/application/third_party/fpdf/font/helveticab.php new file mode 100644 index 0000000..c88394c --- /dev/null +++ b/application/third_party/fpdf/font/helveticab.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/helveticabi.php b/application/third_party/fpdf/font/helveticabi.php new file mode 100644 index 0000000..bcea807 --- /dev/null +++ b/application/third_party/fpdf/font/helveticabi.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/helveticai.php b/application/third_party/fpdf/font/helveticai.php new file mode 100644 index 0000000..a328b04 --- /dev/null +++ b/application/third_party/fpdf/font/helveticai.php @@ -0,0 +1,21 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/symbol.php b/application/third_party/fpdf/font/symbol.php new file mode 100644 index 0000000..5b9147b --- /dev/null +++ b/application/third_party/fpdf/font/symbol.php @@ -0,0 +1,20 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549, + ','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722, + 'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768, + 'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576, + 'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0, + chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603, + chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768, + chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042, + chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329, + chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0); +$uv = array(32=>160,33=>33,34=>8704,35=>35,36=>8707,37=>array(37,2),39=>8715,40=>array(40,2),42=>8727,43=>array(43,2),45=>8722,46=>array(46,18),64=>8773,65=>array(913,2),67=>935,68=>array(916,2),70=>934,71=>915,72=>919,73=>921,74=>977,75=>array(922,4),79=>array(927,2),81=>920,82=>929,83=>array(931,3),86=>962,87=>937,88=>926,89=>936,90=>918,91=>91,92=>8756,93=>93,94=>8869,95=>95,96=>63717,97=>array(945,2),99=>967,100=>array(948,2),102=>966,103=>947,104=>951,105=>953,106=>981,107=>array(954,4),111=>array(959,2),113=>952,114=>961,115=>array(963,3),118=>982,119=>969,120=>958,121=>968,122=>950,123=>array(123,3),126=>8764,160=>8364,161=>978,162=>8242,163=>8804,164=>8725,165=>8734,166=>402,167=>9827,168=>9830,169=>9829,170=>9824,171=>8596,172=>array(8592,4),176=>array(176,2),178=>8243,179=>8805,180=>215,181=>8733,182=>8706,183=>8226,184=>247,185=>array(8800,2),187=>8776,188=>8230,189=>array(63718,2),191=>8629,192=>8501,193=>8465,194=>8476,195=>8472,196=>8855,197=>8853,198=>8709,199=>array(8745,2),201=>8835,202=>8839,203=>8836,204=>8834,205=>8838,206=>array(8712,2),208=>8736,209=>8711,210=>63194,211=>63193,212=>63195,213=>8719,214=>8730,215=>8901,216=>172,217=>array(8743,2),219=>8660,220=>array(8656,4),224=>9674,225=>9001,226=>array(63720,3),229=>8721,230=>array(63723,10),241=>9002,242=>8747,243=>8992,244=>63733,245=>8993,246=>array(63734,9)); +?> diff --git a/application/third_party/fpdf/font/times.php b/application/third_party/fpdf/font/times.php new file mode 100644 index 0000000..f78850f --- /dev/null +++ b/application/third_party/fpdf/font/times.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722, + 'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944, + 'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/timesb.php b/application/third_party/fpdf/font/timesb.php new file mode 100644 index 0000000..0516750 --- /dev/null +++ b/application/third_party/fpdf/font/timesb.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000, + 'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833, + 'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/timesbi.php b/application/third_party/fpdf/font/timesbi.php new file mode 100644 index 0000000..32fe25e --- /dev/null +++ b/application/third_party/fpdf/font/timesbi.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667, + 'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889, + 'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/timesi.php b/application/third_party/fpdf/font/timesi.php new file mode 100644 index 0000000..b0e5a62 --- /dev/null +++ b/application/third_party/fpdf/font/timesi.php @@ -0,0 +1,21 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611, + 'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833, + 'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/application/third_party/fpdf/font/zapfdingbats.php b/application/third_party/fpdf/font/zapfdingbats.php new file mode 100644 index 0000000..b9d0309 --- /dev/null +++ b/application/third_party/fpdf/font/zapfdingbats.php @@ -0,0 +1,20 @@ +0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0, + chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939, + ','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692, + 'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776, + 'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873, + 'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317, + chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788, + chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788, + chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918, + chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874, + chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0); +$uv = array(32=>32,33=>array(9985,4),37=>9742,38=>array(9990,4),42=>9755,43=>9758,44=>array(9996,28),72=>9733,73=>array(10025,35),108=>9679,109=>10061,110=>9632,111=>array(10063,4),115=>9650,116=>9660,117=>9670,118=>10070,119=>9687,120=>array(10072,7),128=>array(10088,14),161=>array(10081,7),168=>9827,169=>9830,170=>9829,171=>9824,172=>array(9312,10),182=>array(10102,31),213=>8594,214=>array(8596,2),216=>array(10136,24),241=>array(10161,14)); +?> diff --git a/application/third_party/fpdf/fpdf.css b/application/third_party/fpdf/fpdf.css new file mode 100644 index 0000000..dd2c540 --- /dev/null +++ b/application/third_party/fpdf/fpdf.css @@ -0,0 +1,21 @@ +body {font-family:"Times New Roman",serif} +h1 {font:bold 135% Arial,sans-serif; color:#4000A0; margin-bottom:0.9em} +h2 {font:bold 95% Arial,sans-serif; color:#900000; margin-top:1.5em; margin-bottom:1em} +dl.param dt {text-decoration:underline} +dl.param dd {margin-top:1em; margin-bottom:1em} +dl.param ul {margin-top:1em; margin-bottom:1em} +tt, code, kbd {font-family:"Courier New",Courier,monospace; font-size:82%} +div.source {margin-top:1.4em; margin-bottom:1.3em} +div.source pre {display:table; border:1px solid #24246A; width:100%; margin:0em; font-family:inherit; font-size:100%} +div.source code {display:block; border:1px solid #C5C5EC; background-color:#F0F5FF; padding:6px; color:#000000} +div.doc-source {margin-top:1.4em; margin-bottom:1.3em} +div.doc-source pre {display:table; width:100%; margin:0em; font-family:inherit; font-size:100%} +div.doc-source code {display:block; background-color:#E0E0E0; padding:4px} +.kw {color:#000080; font-weight:bold} +.str {color:#CC0000} +.cmt {color:#008000} +p.demo {text-align:center; margin-top:-0.9em} +a.demo {text-decoration:none; font-weight:bold; color:#0000CC} +a.demo:link {text-decoration:none; font-weight:bold; color:#0000CC} +a.demo:hover {text-decoration:none; font-weight:bold; color:#0000FF} +a.demo:active {text-decoration:none; font-weight:bold; color:#0000FF} diff --git a/application/third_party/fpdf/fpdf.php b/application/third_party/fpdf/fpdf.php new file mode 100644 index 0000000..ebee958 --- /dev/null +++ b/application/third_party/fpdf/fpdf.php @@ -0,0 +1,1934 @@ +state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + $this->iconv = function_exists('iconv'); + // Font path + if(defined('FPDF_FONTPATH')) + $this->fontpath = FPDF_FONTPATH; + else + $this->fontpath = dirname(__FILE__).'/font/'; + // Core fonts + $this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats'); + // Scale factor + if($unit=='pt') + $this->k = 1; + elseif($unit=='mm') + $this->k = 72/25.4; + elseif($unit=='cm') + $this->k = 72/2.54; + elseif($unit=='in') + $this->k = 72; + else + $this->Error('Incorrect unit: '.$unit); + // Page sizes + $this->StdPageSizes = array('a3'=>array(841.89,1190.55), 'a4'=>array(595.28,841.89), 'a5'=>array(420.94,595.28), + 'letter'=>array(612,792), 'legal'=>array(612,1008)); + $size = $this->_getpagesize($size); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + // Page orientation + $orientation = strtolower($orientation); + if($orientation=='p' || $orientation=='portrait') + { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } + elseif($orientation=='l' || $orientation=='landscape') + { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } + else + $this->Error('Incorrect orientation: '.$orientation); + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + // Page rotation + $this->CurRotation = 0; + // Page margins (1 cm) + $margin = 28.35/$this->k; + $this->SetMargins($margin,$margin); + // Interior cell margin (1 mm) + $this->cMargin = $margin/10; + // Line width (0.2 mm) + $this->LineWidth = .567/$this->k; + // Automatic page break + $this->SetAutoPageBreak(true,2*$margin); + // Default display mode + $this->SetDisplayMode('default'); + // Enable compression + $this->SetCompression(true); + // Metadata + $this->metadata = array('Producer'=>'FPDF '.self::VERSION); + // Set default PDF version number + $this->PDFVersion = '1.3'; +} + +function SetMargins($left, $top, $right=null) +{ + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if($right===null) + $right = $left; + $this->rMargin = $right; +} + +function SetLeftMargin($margin) +{ + // Set left margin + $this->lMargin = $margin; + if($this->page>0 && $this->x<$margin) + $this->x = $margin; +} + +function SetTopMargin($margin) +{ + // Set top margin + $this->tMargin = $margin; +} + +function SetRightMargin($margin) +{ + // Set right margin + $this->rMargin = $margin; +} + +function SetAutoPageBreak($auto, $margin=0) +{ + // Set auto page break mode and triggering margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h-$margin; +} + +function SetDisplayMode($zoom, $layout='default') +{ + // Set display mode in viewer + if($zoom=='fullpage' || $zoom=='fullwidth' || $zoom=='real' || $zoom=='default' || !is_string($zoom)) + $this->ZoomMode = $zoom; + else + $this->Error('Incorrect zoom display mode: '.$zoom); + if($layout=='single' || $layout=='continuous' || $layout=='two' || $layout=='default') + $this->LayoutMode = $layout; + else + $this->Error('Incorrect layout display mode: '.$layout); +} + +function SetCompression($compress) +{ + // Set page compression + if(function_exists('gzcompress')) + $this->compress = $compress; + else + $this->compress = false; +} + +function SetTitle($title, $isUTF8=false) +{ + // Title of document + $this->metadata['Title'] = $isUTF8 ? $title : $this->_UTF8encode($title); +} + +function SetAuthor($author, $isUTF8=false) +{ + // Author of document + $this->metadata['Author'] = $isUTF8 ? $author : $this->_UTF8encode($author); +} + +function SetSubject($subject, $isUTF8=false) +{ + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $subject : $this->_UTF8encode($subject); +} + +function SetKeywords($keywords, $isUTF8=false) +{ + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $keywords : $this->_UTF8encode($keywords); +} + +function SetCreator($creator, $isUTF8=false) +{ + // Creator of document + $this->metadata['Creator'] = $isUTF8 ? $creator : $this->_UTF8encode($creator); +} + +function AliasNbPages($alias='{nb}') +{ + // Define an alias for total number of pages + $this->AliasNbPages = $alias; +} + +function Error($msg) +{ + // Fatal error + throw new Exception('FPDF error: '.$msg); +} + +function Close() +{ + // Terminate document + if($this->state==3) + return; + if($this->page==0) + $this->AddPage(); + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); +} + +function AddPage($orientation='', $size='', $rotation=0) +{ + // Start a new page + if($this->state==3) + $this->Error('The document is closed'); + $family = $this->FontFamily; + $style = $this->FontStyle.($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if($this->page>0) + { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage($orientation,$size,$rotation); + // Set line cap style to square + $this->_out('2 J'); + // Set line width + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w',$lw*$this->k)); + // Set font + if($family) + $this->SetFont($family,$style,$fontsize); + // Set colors + $this->DrawColor = $dc; + if($dc!='0 G') + $this->_out($dc); + $this->FillColor = $fc; + if($fc!='0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if($this->LineWidth!=$lw) + { + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w',$lw*$this->k)); + } + // Restore font + if($family) + $this->SetFont($family,$style,$fontsize); + // Restore colors + if($this->DrawColor!=$dc) + { + $this->DrawColor = $dc; + $this->_out($dc); + } + if($this->FillColor!=$fc) + { + $this->FillColor = $fc; + $this->_out($fc); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; +} + +function Header() +{ + // To be implemented in your own inherited class +} + +function Footer() +{ + // To be implemented in your own inherited class +} + +function PageNo() +{ + // Get current page number + return $this->page; +} + +function SetDrawColor($r, $g=null, $b=null) +{ + // Set color for all stroking operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->DrawColor = sprintf('%.3F G',$r/255); + else + $this->DrawColor = sprintf('%.3F %.3F %.3F RG',$r/255,$g/255,$b/255); + if($this->page>0) + $this->_out($this->DrawColor); +} + +function SetFillColor($r, $g=null, $b=null) +{ + // Set color for all filling operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->FillColor = sprintf('%.3F g',$r/255); + else + $this->FillColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); + if($this->page>0) + $this->_out($this->FillColor); +} + +function SetTextColor($r, $g=null, $b=null) +{ + // Set color for text + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->TextColor = sprintf('%.3F g',$r/255); + else + $this->TextColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); +} + +function GetStringWidth($s) +{ + // Get width of a string in the current font + $cw = $this->CurrentFont['cw']; + $w = 0; + $s = (string)$s; + $l = strlen($s); + for($i=0;$i<$l;$i++) + $w += $cw[$s[$i]]; + return $w*$this->FontSize/1000; +} + +function SetLineWidth($width) +{ + // Set line width + $this->LineWidth = $width; + if($this->page>0) + $this->_out(sprintf('%.2F w',$width*$this->k)); +} + +function Line($x1, $y1, $x2, $y2) +{ + // Draw a line + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S',$x1*$this->k,($this->h-$y1)*$this->k,$x2*$this->k,($this->h-$y2)*$this->k)); +} + +function Rect($x, $y, $w, $h, $style='') +{ + // Draw a rectangle + if($style=='F') + $op = 'f'; + elseif($style=='FD' || $style=='DF') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s',$x*$this->k,($this->h-$y)*$this->k,$w*$this->k,-$h*$this->k,$op)); +} + +function AddFont($family, $style='', $file='', $dir='') +{ + // Add a TrueType, OpenType or Type1 font + $family = strtolower($family); + if($file=='') + $file = str_replace(' ','',$family).strtolower($style).'.php'; + $style = strtoupper($style); + if($style=='IB') + $style = 'BI'; + $fontkey = $family.$style; + if(isset($this->fonts[$fontkey])) + return; + if(strpos($file,'/')!==false || strpos($file,"\\")!==false) + $this->Error('Incorrect font definition file name: '.$file); + if($dir=='') + $dir = $this->fontpath; + if(substr($dir,-1)!='/' && substr($dir,-1)!='\\') + $dir .= '/'; + $info = $this->_loadfont($dir.$file); + $info['i'] = count($this->fonts)+1; + if(!empty($info['file'])) + { + // Embedded font + $info['file'] = $dir.$info['file']; + if($info['type']=='TrueType') + $this->FontFiles[$info['file']] = array('length1'=>$info['originalsize']); + else + $this->FontFiles[$info['file']] = array('length1'=>$info['size1'], 'length2'=>$info['size2']); + } + $this->fonts[$fontkey] = $info; +} + +function SetFont($family, $style='', $size=0) +{ + // Select a font; size given in points + if($family=='') + $family = $this->FontFamily; + else + $family = strtolower($family); + $style = strtoupper($style); + if(strpos($style,'U')!==false) + { + $this->underline = true; + $style = str_replace('U','',$style); + } + else + $this->underline = false; + if($style=='IB') + $style = 'BI'; + if($size==0) + $size = $this->FontSizePt; + // Test if font is already selected + if($this->FontFamily==$family && $this->FontStyle==$style && $this->FontSizePt==$size) + return; + // Test if font is already loaded + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + { + // Test if one of the core fonts + if($family=='arial') + $family = 'helvetica'; + if(in_array($family,$this->CoreFonts)) + { + if($family=='symbol' || $family=='zapfdingbats') + $style = ''; + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + $this->AddFont($family,$style); + } + else + $this->Error('Undefined font: '.$family.' '.$style); + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + $this->CurrentFont = $this->fonts[$fontkey]; + if($this->page>0) + $this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt)); +} + +function SetFontSize($size) +{ + // Set font size in points + if($this->FontSizePt==$size) + return; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + if($this->page>0 && isset($this->CurrentFont)) + $this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt)); +} + +function AddLink() +{ + // Create a new internal link + $n = count($this->links)+1; + $this->links[$n] = array(0, 0); + return $n; +} + +function SetLink($link, $y=0, $page=-1) +{ + // Set destination of internal link + if($y==-1) + $y = $this->y; + if($page==-1) + $page = $this->page; + $this->links[$link] = array($page, $y); +} + +function Link($x, $y, $w, $h, $link) +{ + // Put a link on the page + $this->PageLinks[$this->page][] = array($x*$this->k, $this->hPt-$y*$this->k, $w*$this->k, $h*$this->k, $link); +} + +function Text($x, $y, $txt) +{ + // Output a string + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $txt = (string)$txt; + $s = sprintf('BT %.2F %.2F Td (%s) Tj ET',$x*$this->k,($this->h-$y)*$this->k,$this->_escape($txt)); + if($this->underline && $txt!=='') + $s .= ' '.$this->_dounderline($x,$y,$txt); + if($this->ColorFlag) + $s = 'q '.$this->TextColor.' '.$s.' Q'; + $this->_out($s); +} + +function AcceptPageBreak() +{ + // Accept automatic page break or not + return $this->AutoPageBreak; +} + +function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='') +{ + // Output a cell + $k = $this->k; + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if($ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation); + $this->x = $x; + if($ws>0) + { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw',$ws*$k)); + } + } + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $s = ''; + if($fill || $border==1) + { + if($fill) + $op = ($border==1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ',$this->x*$k,($this->h-$this->y)*$k,$w*$k,-$h*$k,$op); + } + if(is_string($border)) + { + $x = $this->x; + $y = $this->y; + if(strpos($border,'L')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,$x*$k,($this->h-($y+$h))*$k); + if(strpos($border,'T')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-$y)*$k); + if(strpos($border,'R')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',($x+$w)*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-($y+$h))*$k); + if(strpos($border,'B')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-($y+$h))*$k,($x+$w)*$k,($this->h-($y+$h))*$k); + } + $txt = (string)$txt; + if($txt!=='') + { + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if($align=='R') + $dx = $w-$this->cMargin-$this->GetStringWidth($txt); + elseif($align=='C') + $dx = ($w-$this->GetStringWidth($txt))/2; + else + $dx = $this->cMargin; + if($this->ColorFlag) + $s .= 'q '.$this->TextColor.' '; + $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k,$this->_escape($txt)); + if($this->underline) + $s .= ' '.$this->_dounderline($this->x+$dx,$this->y+.5*$h+.3*$this->FontSize,$txt); + if($this->ColorFlag) + $s .= ' Q'; + if($link) + $this->Link($this->x+$dx,$this->y+.5*$h-.5*$this->FontSize,$this->GetStringWidth($txt),$this->FontSize,$link); + } + if($s) + $this->_out($s); + $this->lasth = $h; + if($ln>0) + { + // Go to next line + $this->y += $h; + if($ln==1) + $this->x = $this->lMargin; + } + else + $this->x += $w; +} + +function MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false) +{ + // Output text with automatic or explicit line breaks + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r",'',(string)$txt); + $nb = strlen($s); + if($nb>0 && $s[$nb-1]=="\n") + $nb--; + $b = 0; + if($border) + { + if($border==1) + { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } + else + { + $b2 = ''; + if(strpos($border,'L')!==false) + $b2 .= 'L'; + if(strpos($border,'R')!==false) + $b2 .= 'R'; + $b = (strpos($border,'T')!==false) ? $b2.'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + $c = $s[$i]; + if($c=="\n") + { + // Explicit line break + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + continue; + } + if($c==' ') + { + $sep = $i; + $ls = $l; + $ns++; + } + $l += $cw[$c]; + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($i==$j) + $i++; + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + } + else + { + if($align=='J') + { + $this->ws = ($ns>1) ? ($wmax-$ls)/1000*$this->FontSize/($ns-1) : 0; + $this->_out(sprintf('%.3F Tw',$this->ws*$this->k)); + } + $this->Cell($w,$h,substr($s,$j,$sep-$j),$b,2,$align,$fill); + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + } + else + $i++; + } + // Last chunk + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + if($border && strpos($border,'B')!==false) + $b .= 'B'; + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + $this->x = $this->lMargin; +} + +function Write($h, $txt, $link='') +{ + // Output text in flowing mode + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r",'',(string)$txt); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + $c = $s[$i]; + if($c=="\n") + { + // Explicit line break + $this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link); + $i++; + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + } + $nl++; + continue; + } + if($c==' ') + $sep = $i; + $l += $cw[$c]; + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($this->x>$this->lMargin) + { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $i++; + $nl++; + continue; + } + if($i==$j) + $i++; + $this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link); + } + else + { + $this->Cell($w,$h,substr($s,$j,$sep-$j),0,2,'',false,$link); + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + } + $nl++; + } + else + $i++; + } + // Last chunk + if($i!=$j) + $this->Cell($l/1000*$this->FontSize,$h,substr($s,$j),0,0,'',false,$link); +} + +function Ln($h=null) +{ + // Line feed; default value is the last cell height + $this->x = $this->lMargin; + if($h===null) + $this->y += $this->lasth; + else + $this->y += $h; +} + +function Image($file, $x=null, $y=null, $w=0, $h=0, $type='', $link='') +{ + // Put an image on the page + if($file=='') + $this->Error('Image file name is empty'); + if(!isset($this->images[$file])) + { + // First use of this image, get info + if($type=='') + { + $pos = strrpos($file,'.'); + if(!$pos) + $this->Error('Image file has no extension and no type was specified: '.$file); + $type = substr($file,$pos+1); + } + $type = strtolower($type); + if($type=='jpeg') + $type = 'jpg'; + $mtd = '_parse'.$type; + if(!method_exists($this,$mtd)) + $this->Error('Unsupported image type: '.$type); + $info = $this->$mtd($file); + $info['i'] = count($this->images)+1; + $this->images[$file] = $info; + } + else + $info = $this->images[$file]; + + // Automatic width and height calculation if needed + if($w==0 && $h==0) + { + // Put image at 96 dpi + $w = -96; + $h = -96; + } + if($w<0) + $w = -$info['w']*72/$w/$this->k; + if($h<0) + $h = -$info['h']*72/$h/$this->k; + if($w==0) + $w = $h*$info['w']/$info['h']; + if($h==0) + $h = $w*$info['h']/$info['w']; + + // Flowing mode + if($y===null) + { + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x2 = $this->x; + $this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation); + $this->x = $x2; + } + $y = $this->y; + $this->y += $h; + } + + if($x===null) + $x = $this->x; + $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q',$w*$this->k,$h*$this->k,$x*$this->k,($this->h-($y+$h))*$this->k,$info['i'])); + if($link) + $this->Link($x,$y,$w,$h,$link); +} + +function GetPageWidth() +{ + // Get current page width + return $this->w; +} + +function GetPageHeight() +{ + // Get current page height + return $this->h; +} + +function GetX() +{ + // Get x position + return $this->x; +} + +function SetX($x) +{ + // Set x position + if($x>=0) + $this->x = $x; + else + $this->x = $this->w+$x; +} + +function GetY() +{ + // Get y position + return $this->y; +} + +function SetY($y, $resetX=true) +{ + // Set y position and optionally reset x + if($y>=0) + $this->y = $y; + else + $this->y = $this->h+$y; + if($resetX) + $this->x = $this->lMargin; +} + +function SetXY($x, $y) +{ + // Set x and y positions + $this->SetX($x); + $this->SetY($y,false); +} + +function Output($dest='', $name='', $isUTF8=false) +{ + // Output PDF to some destination + $this->Close(); + if(strlen($name)==1 && strlen($dest)!=1) + { + // Fix parameter order + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if($dest=='') + $dest = 'I'; + if($name=='') + $name = 'doc.pdf'; + switch(strtoupper($dest)) + { + case 'I': + // Send to standard output + $this->_checkoutput(); + if(PHP_SAPI!='cli') + { + // We send to a browser + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; '.$this->_httpencode('filename',$name,$isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; '.$this->_httpencode('filename',$name,$isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + echo $this->buffer; + break; + case 'F': + // Save to local file + if(!file_put_contents($name,$this->buffer)) + $this->Error('Unable to create output file: '.$name); + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error('Incorrect output destination: '.$dest); + } + return ''; +} + +/******************************************************************************* +* Protected methods * +*******************************************************************************/ + +protected function _checkoutput() +{ + if(PHP_SAPI!='cli') + { + if(headers_sent($file,$line)) + $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); + } + if(ob_get_length()) + { + // The output buffer is not empty + if(preg_match('/^(\xEF\xBB\xBF)?\s*$/',ob_get_contents())) + { + // It contains only a UTF-8 BOM and/or whitespace, let's clean it + ob_clean(); + } + else + $this->Error("Some data has already been output, can't send PDF file"); + } +} + +protected function _getpagesize($size) +{ + if(is_string($size)) + { + $size = strtolower($size); + if(!isset($this->StdPageSizes[$size])) + $this->Error('Unknown page size: '.$size); + $a = $this->StdPageSizes[$size]; + return array($a[0]/$this->k, $a[1]/$this->k); + } + else + { + if($size[0]>$size[1]) + return array($size[1], $size[0]); + else + return $size; + } +} + +protected function _beginpage($orientation, $size, $rotation) +{ + $this->page++; + $this->pages[$this->page] = ''; + $this->PageLinks[$this->page] = array(); + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if($orientation=='') + $orientation = $this->DefOrientation; + else + $orientation = strtoupper($orientation[0]); + if($size=='') + $size = $this->DefPageSize; + else + $size = $this->_getpagesize($size); + if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1]) + { + // New size or orientation + if($orientation=='P') + { + $this->w = $size[0]; + $this->h = $size[1]; + } + else + { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + $this->PageBreakTrigger = $this->h-$this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if($orientation!=$this->DefOrientation || $size[0]!=$this->DefPageSize[0] || $size[1]!=$this->DefPageSize[1]) + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + if($rotation!=0) + { + if($rotation%90!=0) + $this->Error('Incorrect rotation value: '.$rotation); + $this->PageInfo[$this->page]['rotation'] = $rotation; + } + $this->CurRotation = $rotation; +} + +protected function _endpage() +{ + $this->state = 1; +} + +protected function _loadfont($path) +{ + // Load a font definition file + include($path); + if(!isset($name)) + $this->Error('Could not include font definition file: '.$path); + if(isset($enc)) + $enc = strtolower($enc); + if(!isset($subsetted)) + $subsetted = false; + return get_defined_vars(); +} + +protected function _isascii($s) +{ + // Test if string is ASCII + $nb = strlen($s); + for($i=0;$i<$nb;$i++) + { + if(ord($s[$i])>127) + return false; + } + return true; +} + +protected function _httpencode($param, $value, $isUTF8) +{ + // Encode HTTP header field parameter + if($this->_isascii($value)) + return $param.'="'.$value.'"'; + if(!$isUTF8) + $value = $this->_UTF8encode($value); + return $param."*=UTF-8''".rawurlencode($value); +} + +protected function _UTF8encode($s) +{ + // Convert ISO-8859-1 to UTF-8 + if($this->iconv) + return iconv('ISO-8859-1','UTF-8',$s); + $res = ''; + $nb = strlen($s); + for($i=0;$i<$nb;$i++) + { + $c = $s[$i]; + $v = ord($c); + if($v>=128) + { + $res .= chr(0xC0 | ($v >> 6)); + $res .= chr(0x80 | ($v & 0x3F)); + } + else + $res .= $c; + } + return $res; +} + +protected function _UTF8toUTF16($s) +{ + // Convert UTF-8 to UTF-16BE with BOM + $res = "\xFE\xFF"; + if($this->iconv) + return $res.iconv('UTF-8','UTF-16BE',$s); + $nb = strlen($s); + $i = 0; + while($i<$nb) + { + $c1 = ord($s[$i++]); + if($c1>=224) + { + // 3-byte character + $c2 = ord($s[$i++]); + $c3 = ord($s[$i++]); + $res .= chr((($c1 & 0x0F)<<4) + (($c2 & 0x3C)>>2)); + $res .= chr((($c2 & 0x03)<<6) + ($c3 & 0x3F)); + } + elseif($c1>=192) + { + // 2-byte character + $c2 = ord($s[$i++]); + $res .= chr(($c1 & 0x1C)>>2); + $res .= chr((($c1 & 0x03)<<6) + ($c2 & 0x3F)); + } + else + { + // Single-byte character + $res .= "\0".chr($c1); + } + } + return $res; +} + +protected function _escape($s) +{ + // Escape special characters + if(strpos($s,'(')!==false || strpos($s,')')!==false || strpos($s,'\\')!==false || strpos($s,"\r")!==false) + return str_replace(array('\\','(',')',"\r"), array('\\\\','\\(','\\)','\\r'), $s); + else + return $s; +} + +protected function _textstring($s) +{ + // Format a text string + if(!$this->_isascii($s)) + $s = $this->_UTF8toUTF16($s); + return '('.$this->_escape($s).')'; +} + +protected function _dounderline($x, $y, $txt) +{ + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth($txt)+$this->ws*substr_count($txt,' '); + return sprintf('%.2F %.2F %.2F %.2F re f',$x*$this->k,($this->h-($y-$up/1000*$this->FontSize))*$this->k,$w*$this->k,-$ut/1000*$this->FontSizePt); +} + +protected function _parsejpg($file) +{ + // Extract info from a JPEG file + $a = getimagesize($file); + if(!$a) + $this->Error('Missing or incorrect image file: '.$file); + if($a[2]!=2) + $this->Error('Not a JPEG file: '.$file); + if(!isset($a['channels']) || $a['channels']==3) + $colspace = 'DeviceRGB'; + elseif($a['channels']==4) + $colspace = 'DeviceCMYK'; + else + $colspace = 'DeviceGray'; + $bpc = isset($a['bits']) ? $a['bits'] : 8; + $data = file_get_contents($file); + return array('w'=>$a[0], 'h'=>$a[1], 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'DCTDecode', 'data'=>$data); +} + +protected function _parsepng($file) +{ + // Extract info from a PNG file + $f = fopen($file,'rb'); + if(!$f) + $this->Error('Can\'t open image file: '.$file); + $info = $this->_parsepngstream($f,$file); + fclose($f); + return $info; +} + +protected function _parsepngstream($f, $file) +{ + // Check signature + if($this->_readstream($f,8)!=chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10)) + $this->Error('Not a PNG file: '.$file); + + // Read header chunk + $this->_readstream($f,4); + if($this->_readstream($f,4)!='IHDR') + $this->Error('Incorrect PNG file: '.$file); + $w = $this->_readint($f); + $h = $this->_readint($f); + $bpc = ord($this->_readstream($f,1)); + if($bpc>8) + $this->Error('16-bit depth not supported: '.$file); + $ct = ord($this->_readstream($f,1)); + if($ct==0 || $ct==4) + $colspace = 'DeviceGray'; + elseif($ct==2 || $ct==6) + $colspace = 'DeviceRGB'; + elseif($ct==3) + $colspace = 'Indexed'; + else + $this->Error('Unknown color type: '.$file); + if(ord($this->_readstream($f,1))!=0) + $this->Error('Unknown compression method: '.$file); + if(ord($this->_readstream($f,1))!=0) + $this->Error('Unknown filter method: '.$file); + if(ord($this->_readstream($f,1))!=0) + $this->Error('Interlacing not supported: '.$file); + $this->_readstream($f,4); + $dp = '/Predictor 15 /Colors '.($colspace=='DeviceRGB' ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w; + + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do + { + $n = $this->_readint($f); + $type = $this->_readstream($f,4); + if($type=='PLTE') + { + // Read palette + $pal = $this->_readstream($f,$n); + $this->_readstream($f,4); + } + elseif($type=='tRNS') + { + // Read transparency info + $t = $this->_readstream($f,$n); + if($ct==0) + $trns = array(ord(substr($t,1,1))); + elseif($ct==2) + $trns = array(ord(substr($t,1,1)), ord(substr($t,3,1)), ord(substr($t,5,1))); + else + { + $pos = strpos($t,chr(0)); + if($pos!==false) + $trns = array($pos); + } + $this->_readstream($f,4); + } + elseif($type=='IDAT') + { + // Read image data block + $data .= $this->_readstream($f,$n); + $this->_readstream($f,4); + } + elseif($type=='IEND') + break; + else + $this->_readstream($f,$n+4); + } + while($n); + + if($colspace=='Indexed' && empty($pal)) + $this->Error('Missing palette in '.$file); + $info = array('w'=>$w, 'h'=>$h, 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'FlateDecode', 'dp'=>$dp, 'pal'=>$pal, 'trns'=>$trns); + if($ct>=4) + { + // Extract alpha channel + if(!function_exists('gzuncompress')) + $this->Error('Zlib not available, can\'t handle alpha channel: '.$file); + $data = gzuncompress($data); + $color = ''; + $alpha = ''; + if($ct==4) + { + // Gray image + $len = 2*$w; + for($i=0;$i<$h;$i++) + { + $pos = (1+$len)*$i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data,$pos+1,$len); + $color .= preg_replace('/(.)./s','$1',$line); + $alpha .= preg_replace('/.(.)/s','$1',$line); + } + } + else + { + // RGB image + $len = 4*$w; + for($i=0;$i<$h;$i++) + { + $pos = (1+$len)*$i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data,$pos+1,$len); + $color .= preg_replace('/(.{3})./s','$1',$line); + $alpha .= preg_replace('/.{3}(.)/s','$1',$line); + } + } + unset($data); + $data = gzcompress($color); + $info['smask'] = gzcompress($alpha); + $this->WithAlpha = true; + if($this->PDFVersion<'1.4') + $this->PDFVersion = '1.4'; + } + $info['data'] = $data; + return $info; +} + +protected function _readstream($f, $n) +{ + // Read n bytes from stream + $res = ''; + while($n>0 && !feof($f)) + { + $s = fread($f,$n); + if($s===false) + $this->Error('Error while reading stream'); + $n -= strlen($s); + $res .= $s; + } + if($n>0) + $this->Error('Unexpected end of stream'); + return $res; +} + +protected function _readint($f) +{ + // Read a 4-byte integer from stream + $a = unpack('Ni',$this->_readstream($f,4)); + return $a['i']; +} + +protected function _parsegif($file) +{ + // Extract info from a GIF file (via PNG conversion) + if(!function_exists('imagepng')) + $this->Error('GD extension is required for GIF support'); + if(!function_exists('imagecreatefromgif')) + $this->Error('GD has no GIF read support'); + $im = imagecreatefromgif($file); + if(!$im) + $this->Error('Missing or incorrect image file: '.$file); + imageinterlace($im,0); + ob_start(); + imagepng($im); + $data = ob_get_clean(); + imagedestroy($im); + $f = fopen('php://temp','rb+'); + if(!$f) + $this->Error('Unable to create memory stream'); + fwrite($f,$data); + rewind($f); + $info = $this->_parsepngstream($f,$file); + fclose($f); + return $info; +} + +protected function _out($s) +{ + // Add a line to the current page + if($this->state==2) + $this->pages[$this->page] .= $s."\n"; + elseif($this->state==0) + $this->Error('No page has been added yet'); + elseif($this->state==1) + $this->Error('Invalid call'); + elseif($this->state==3) + $this->Error('The document is closed'); +} + +protected function _put($s) +{ + // Add a line to the document + $this->buffer .= $s."\n"; +} + +protected function _getoffset() +{ + return strlen($this->buffer); +} + +protected function _newobj($n=null) +{ + // Begin a new object + if($n===null) + $n = ++$this->n; + $this->offsets[$n] = $this->_getoffset(); + $this->_put($n.' 0 obj'); +} + +protected function _putstream($data) +{ + $this->_put('stream'); + $this->_put($data); + $this->_put('endstream'); +} + +protected function _putstreamobject($data) +{ + if($this->compress) + { + $entries = '/Filter /FlateDecode '; + $data = gzcompress($data); + } + else + $entries = ''; + $entries .= '/Length '.strlen($data); + $this->_newobj(); + $this->_put('<<'.$entries.'>>'); + $this->_putstream($data); + $this->_put('endobj'); +} + +protected function _putlinks($n) +{ + foreach($this->PageLinks[$n] as $pl) + { + $this->_newobj(); + $rect = sprintf('%.2F %.2F %.2F %.2F',$pl[0],$pl[1],$pl[0]+$pl[2],$pl[1]-$pl[3]); + $s = '<_textstring($pl[4]).'>>>>'; + else + { + $l = $this->links[$pl[4]]; + if(isset($this->PageInfo[$l[0]]['size'])) + $h = $this->PageInfo[$l[0]]['size'][1]; + else + $h = ($this->DefOrientation=='P') ? $this->DefPageSize[1]*$this->k : $this->DefPageSize[0]*$this->k; + $s .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>',$this->PageInfo[$l[0]]['n'],$h-$l[1]*$this->k); + } + $this->_put($s); + $this->_put('endobj'); + } +} + +protected function _putpage($n) +{ + $this->_newobj(); + $this->_put('<_put('/Parent 1 0 R'); + if(isset($this->PageInfo[$n]['size'])) + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$this->PageInfo[$n]['size'][0],$this->PageInfo[$n]['size'][1])); + if(isset($this->PageInfo[$n]['rotation'])) + $this->_put('/Rotate '.$this->PageInfo[$n]['rotation']); + $this->_put('/Resources 2 0 R'); + if(!empty($this->PageLinks[$n])) + { + $s = '/Annots ['; + foreach($this->PageLinks[$n] as $pl) + $s .= $pl[5].' 0 R '; + $s .= ']'; + $this->_put($s); + } + if($this->WithAlpha) + $this->_put('/Group <>'); + $this->_put('/Contents '.($this->n+1).' 0 R>>'); + $this->_put('endobj'); + // Page content + if(!empty($this->AliasNbPages)) + $this->pages[$n] = str_replace($this->AliasNbPages,$this->page,$this->pages[$n]); + $this->_putstreamobject($this->pages[$n]); + // Link annotations + $this->_putlinks($n); +} + +protected function _putpages() +{ + $nb = $this->page; + $n = $this->n; + for($i=1;$i<=$nb;$i++) + { + $this->PageInfo[$i]['n'] = ++$n; + $n++; + foreach($this->PageLinks[$i] as &$pl) + $pl[5] = ++$n; + unset($pl); + } + for($i=1;$i<=$nb;$i++) + $this->_putpage($i); + // Pages root + $this->_newobj(1); + $this->_put('<PageInfo[$i]['n'].' 0 R '; + $kids .= ']'; + $this->_put($kids); + $this->_put('/Count '.$nb); + if($this->DefOrientation=='P') + { + $w = $this->DefPageSize[0]; + $h = $this->DefPageSize[1]; + } + else + { + $w = $this->DefPageSize[1]; + $h = $this->DefPageSize[0]; + } + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$w*$this->k,$h*$this->k)); + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putfonts() +{ + foreach($this->FontFiles as $file=>$info) + { + // Font file embedding + $this->_newobj(); + $this->FontFiles[$file]['n'] = $this->n; + $font = file_get_contents($file); + if(!$font) + $this->Error('Font file not found: '.$file); + $compressed = (substr($file,-2)=='.z'); + if(!$compressed && isset($info['length2'])) + $font = substr($font,6,$info['length1']).substr($font,6+$info['length1']+6,$info['length2']); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('/Length1 '.$info['length1']); + if(isset($info['length2'])) + $this->_put('/Length2 '.$info['length2'].' /Length3 0'); + $this->_put('>>'); + $this->_putstream($font); + $this->_put('endobj'); + } + foreach($this->fonts as $k=>$font) + { + // Encoding + if(isset($font['diff'])) + { + if(!isset($this->encodings[$font['enc']])) + { + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + $this->encodings[$font['enc']] = $this->n; + } + } + // ToUnicode CMap + if(isset($font['uv'])) + { + if(isset($font['enc'])) + $cmapkey = $font['enc']; + else + $cmapkey = $font['name']; + if(!isset($this->cmaps[$cmapkey])) + { + $cmap = $this->_tounicodecmap($font['uv']); + $this->_putstreamobject($cmap); + $this->cmaps[$cmapkey] = $this->n; + } + } + // Font object + $this->fonts[$k]['n'] = $this->n+1; + $type = $font['type']; + $name = $font['name']; + if($font['subsetted']) + $name = 'AAAAAA+'.$name; + if($type=='Core') + { + // Core font + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$name); + $this->_put('/Subtype /Type1'); + if($name!='Symbol' && $name!='ZapfDingbats') + $this->_put('/Encoding /WinAnsiEncoding'); + if(isset($font['uv'])) + $this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + } + elseif($type=='Type1' || $type=='TrueType') + { + // Additional Type1 or TrueType/OpenType font + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$name); + $this->_put('/Subtype /'.$type); + $this->_put('/FirstChar 32 /LastChar 255'); + $this->_put('/Widths '.($this->n+1).' 0 R'); + $this->_put('/FontDescriptor '.($this->n+2).' 0 R'); + if(isset($font['diff'])) + $this->_put('/Encoding '.$this->encodings[$font['enc']].' 0 R'); + else + $this->_put('/Encoding /WinAnsiEncoding'); + if(isset($font['uv'])) + $this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + // Widths + $this->_newobj(); + $cw = $font['cw']; + $s = '['; + for($i=32;$i<=255;$i++) + $s .= $cw[chr($i)].' '; + $this->_put($s.']'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '<$v) + $s .= ' /'.$k.' '.$v; + if(!empty($font['file'])) + $s .= ' /FontFile'.($type=='Type1' ? '' : '2').' '.$this->FontFiles[$font['file']]['n'].' 0 R'; + $this->_put($s.'>>'); + $this->_put('endobj'); + } + else + { + // Allow for additional types + $mtd = '_put'.strtolower($type); + if(!method_exists($this,$mtd)) + $this->Error('Unsupported font type: '.$type); + $this->$mtd($font); + } + } +} + +protected function _tounicodecmap($uv) +{ + $ranges = ''; + $nbr = 0; + $chars = ''; + $nbc = 0; + foreach($uv as $c=>$v) + { + if(is_array($v)) + { + $ranges .= sprintf("<%02X> <%02X> <%04X>\n",$c,$c+$v[1]-1,$v[0]); + $nbr++; + } + else + { + $chars .= sprintf("<%02X> <%04X>\n",$c,$v); + $nbc++; + } + } + $s = "/CIDInit /ProcSet findresource begin\n"; + $s .= "12 dict begin\n"; + $s .= "begincmap\n"; + $s .= "/CIDSystemInfo\n"; + $s .= "<0) + { + $s .= "$nbr beginbfrange\n"; + $s .= $ranges; + $s .= "endbfrange\n"; + } + if($nbc>0) + { + $s .= "$nbc beginbfchar\n"; + $s .= $chars; + $s .= "endbfchar\n"; + } + $s .= "endcmap\n"; + $s .= "CMapName currentdict /CMap defineresource pop\n"; + $s .= "end\n"; + $s .= "end"; + return $s; +} + +protected function _putimages() +{ + foreach(array_keys($this->images) as $file) + { + $this->_putimage($this->images[$file]); + unset($this->images[$file]['data']); + unset($this->images[$file]['smask']); + } +} + +protected function _putimage(&$info) +{ + $this->_newobj(); + $info['n'] = $this->n; + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width '.$info['w']); + $this->_put('/Height '.$info['h']); + if($info['cs']=='Indexed') + $this->_put('/ColorSpace [/Indexed /DeviceRGB '.(strlen($info['pal'])/3-1).' '.($this->n+1).' 0 R]'); + else + { + $this->_put('/ColorSpace /'.$info['cs']); + if($info['cs']=='DeviceCMYK') + $this->_put('/Decode [1 0 1 0 1 0 1 0]'); + } + $this->_put('/BitsPerComponent '.$info['bpc']); + if(isset($info['f'])) + $this->_put('/Filter /'.$info['f']); + if(isset($info['dp'])) + $this->_put('/DecodeParms <<'.$info['dp'].'>>'); + if(isset($info['trns']) && is_array($info['trns'])) + { + $trns = ''; + for($i=0;$i_put('/Mask ['.$trns.']'); + } + if(isset($info['smask'])) + $this->_put('/SMask '.($this->n+1).' 0 R'); + $this->_put('/Length '.strlen($info['data']).'>>'); + $this->_putstream($info['data']); + $this->_put('endobj'); + // Soft mask + if(isset($info['smask'])) + { + $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns '.$info['w']; + $smask = array('w'=>$info['w'], 'h'=>$info['h'], 'cs'=>'DeviceGray', 'bpc'=>8, 'f'=>$info['f'], 'dp'=>$dp, 'data'=>$info['smask']); + $this->_putimage($smask); + } + // Palette + if($info['cs']=='Indexed') + $this->_putstreamobject($info['pal']); +} + +protected function _putxobjectdict() +{ + foreach($this->images as $image) + $this->_put('/I'.$image['i'].' '.$image['n'].' 0 R'); +} + +protected function _putresourcedict() +{ + $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_put('/Font <<'); + foreach($this->fonts as $font) + $this->_put('/F'.$font['i'].' '.$font['n'].' 0 R'); + $this->_put('>>'); + $this->_put('/XObject <<'); + $this->_putxobjectdict(); + $this->_put('>>'); +} + +protected function _putresources() +{ + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj(2); + $this->_put('<<'); + $this->_putresourcedict(); + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putinfo() +{ + $date = @date('YmdHisO',$this->CreationDate); + $this->metadata['CreationDate'] = 'D:'.substr($date,0,-2)."'".substr($date,-2)."'"; + foreach($this->metadata as $key=>$value) + $this->_put('/'.$key.' '.$this->_textstring($value)); +} + +protected function _putcatalog() +{ + $n = $this->PageInfo[1]['n']; + $this->_put('/Type /Catalog'); + $this->_put('/Pages 1 0 R'); + if($this->ZoomMode=='fullpage') + $this->_put('/OpenAction ['.$n.' 0 R /Fit]'); + elseif($this->ZoomMode=='fullwidth') + $this->_put('/OpenAction ['.$n.' 0 R /FitH null]'); + elseif($this->ZoomMode=='real') + $this->_put('/OpenAction ['.$n.' 0 R /XYZ null null 1]'); + elseif(!is_string($this->ZoomMode)) + $this->_put('/OpenAction ['.$n.' 0 R /XYZ null null '.sprintf('%.2F',$this->ZoomMode/100).']'); + if($this->LayoutMode=='single') + $this->_put('/PageLayout /SinglePage'); + elseif($this->LayoutMode=='continuous') + $this->_put('/PageLayout /OneColumn'); + elseif($this->LayoutMode=='two') + $this->_put('/PageLayout /TwoColumnLeft'); +} + +protected function _putheader() +{ + $this->_put('%PDF-'.$this->PDFVersion); +} + +protected function _puttrailer() +{ + $this->_put('/Size '.($this->n+1)); + $this->_put('/Root '.$this->n.' 0 R'); + $this->_put('/Info '.($this->n-1).' 0 R'); +} + +protected function _enddoc() +{ + $this->CreationDate = time(); + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + // Info + $this->_newobj(); + $this->_put('<<'); + $this->_putinfo(); + $this->_put('>>'); + $this->_put('endobj'); + // Catalog + $this->_newobj(); + $this->_put('<<'); + $this->_putcatalog(); + $this->_put('>>'); + $this->_put('endobj'); + // Cross-ref + $offset = $this->_getoffset(); + $this->_put('xref'); + $this->_put('0 '.($this->n+1)); + $this->_put('0000000000 65535 f '); + for($i=1;$i<=$this->n;$i++) + $this->_put(sprintf('%010d 00000 n ',$this->offsets[$i])); + // Trailer + $this->_put('trailer'); + $this->_put('<<'); + $this->_puttrailer(); + $this->_put('>>'); + $this->_put('startxref'); + $this->_put($offset); + $this->_put('%%EOF'); + $this->state = 3; +} +} +?> diff --git a/application/third_party/fpdf/install.txt b/application/third_party/fpdf/install.txt new file mode 100644 index 0000000..73ded64 --- /dev/null +++ b/application/third_party/fpdf/install.txt @@ -0,0 +1,15 @@ +The FPDF library is made up of the following elements: + +- the main file, fpdf.php, which contains the class +- the font definition files located in the font directory + +The font definition files are necessary as soon as you want to output some text in a document. +If they are not accessible, the SetFont() method will produce the following error: + +FPDF error: Could not include font definition file + + +Remarks: + +- Only the files corresponding to the fonts actually used are necessary +- The tutorials provided in this package are ready to be executed diff --git a/application/third_party/fpdf/license.txt b/application/third_party/fpdf/license.txt new file mode 100644 index 0000000..fd811c6 --- /dev/null +++ b/application/third_party/fpdf/license.txt @@ -0,0 +1,6 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software to use, copy, modify, distribute, sublicense, and/or sell +copies of the software, and to permit persons to whom the software is furnished +to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. \ No newline at end of file diff --git a/application/third_party/fpdf/makefont/cp1250.map b/application/third_party/fpdf/makefont/cp1250.map new file mode 100644 index 0000000..ec110af --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1250.map @@ -0,0 +1,251 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!89 U+2030 perthousand +!8A U+0160 Scaron +!8B U+2039 guilsinglleft +!8C U+015A Sacute +!8D U+0164 Tcaron +!8E U+017D Zcaron +!8F U+0179 Zacute +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!99 U+2122 trademark +!9A U+0161 scaron +!9B U+203A guilsinglright +!9C U+015B sacute +!9D U+0165 tcaron +!9E U+017E zcaron +!9F U+017A zacute +!A0 U+00A0 space +!A1 U+02C7 caron +!A2 U+02D8 breve +!A3 U+0141 Lslash +!A4 U+00A4 currency +!A5 U+0104 Aogonek +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+015E Scedilla +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+017B Zdotaccent +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+02DB ogonek +!B3 U+0142 lslash +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+0105 aogonek +!BA U+015F scedilla +!BB U+00BB guillemotright +!BC U+013D Lcaron +!BD U+02DD hungarumlaut +!BE U+013E lcaron +!BF U+017C zdotaccent +!C0 U+0154 Racute +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+0102 Abreve +!C4 U+00C4 Adieresis +!C5 U+0139 Lacute +!C6 U+0106 Cacute +!C7 U+00C7 Ccedilla +!C8 U+010C Ccaron +!C9 U+00C9 Eacute +!CA U+0118 Eogonek +!CB U+00CB Edieresis +!CC U+011A Ecaron +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+010E Dcaron +!D0 U+0110 Dcroat +!D1 U+0143 Nacute +!D2 U+0147 Ncaron +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+0150 Ohungarumlaut +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+0158 Rcaron +!D9 U+016E Uring +!DA U+00DA Uacute +!DB U+0170 Uhungarumlaut +!DC U+00DC Udieresis +!DD U+00DD Yacute +!DE U+0162 Tcommaaccent +!DF U+00DF germandbls +!E0 U+0155 racute +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+0103 abreve +!E4 U+00E4 adieresis +!E5 U+013A lacute +!E6 U+0107 cacute +!E7 U+00E7 ccedilla +!E8 U+010D ccaron +!E9 U+00E9 eacute +!EA U+0119 eogonek +!EB U+00EB edieresis +!EC U+011B ecaron +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+010F dcaron +!F0 U+0111 dcroat +!F1 U+0144 nacute +!F2 U+0148 ncaron +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+0151 ohungarumlaut +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+0159 rcaron +!F9 U+016F uring +!FA U+00FA uacute +!FB U+0171 uhungarumlaut +!FC U+00FC udieresis +!FD U+00FD yacute +!FE U+0163 tcommaaccent +!FF U+02D9 dotaccent diff --git a/application/third_party/fpdf/makefont/cp1251.map b/application/third_party/fpdf/makefont/cp1251.map new file mode 100644 index 0000000..de6a198 --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1251.map @@ -0,0 +1,255 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0402 afii10051 +!81 U+0403 afii10052 +!82 U+201A quotesinglbase +!83 U+0453 afii10100 +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!88 U+20AC Euro +!89 U+2030 perthousand +!8A U+0409 afii10058 +!8B U+2039 guilsinglleft +!8C U+040A afii10059 +!8D U+040C afii10061 +!8E U+040B afii10060 +!8F U+040F afii10145 +!90 U+0452 afii10099 +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!99 U+2122 trademark +!9A U+0459 afii10106 +!9B U+203A guilsinglright +!9C U+045A afii10107 +!9D U+045C afii10109 +!9E U+045B afii10108 +!9F U+045F afii10193 +!A0 U+00A0 space +!A1 U+040E afii10062 +!A2 U+045E afii10110 +!A3 U+0408 afii10057 +!A4 U+00A4 currency +!A5 U+0490 afii10050 +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+0401 afii10023 +!A9 U+00A9 copyright +!AA U+0404 afii10053 +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+0407 afii10056 +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+0406 afii10055 +!B3 U+0456 afii10103 +!B4 U+0491 afii10098 +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+0451 afii10071 +!B9 U+2116 afii61352 +!BA U+0454 afii10101 +!BB U+00BB guillemotright +!BC U+0458 afii10105 +!BD U+0405 afii10054 +!BE U+0455 afii10102 +!BF U+0457 afii10104 +!C0 U+0410 afii10017 +!C1 U+0411 afii10018 +!C2 U+0412 afii10019 +!C3 U+0413 afii10020 +!C4 U+0414 afii10021 +!C5 U+0415 afii10022 +!C6 U+0416 afii10024 +!C7 U+0417 afii10025 +!C8 U+0418 afii10026 +!C9 U+0419 afii10027 +!CA U+041A afii10028 +!CB U+041B afii10029 +!CC U+041C afii10030 +!CD U+041D afii10031 +!CE U+041E afii10032 +!CF U+041F afii10033 +!D0 U+0420 afii10034 +!D1 U+0421 afii10035 +!D2 U+0422 afii10036 +!D3 U+0423 afii10037 +!D4 U+0424 afii10038 +!D5 U+0425 afii10039 +!D6 U+0426 afii10040 +!D7 U+0427 afii10041 +!D8 U+0428 afii10042 +!D9 U+0429 afii10043 +!DA U+042A afii10044 +!DB U+042B afii10045 +!DC U+042C afii10046 +!DD U+042D afii10047 +!DE U+042E afii10048 +!DF U+042F afii10049 +!E0 U+0430 afii10065 +!E1 U+0431 afii10066 +!E2 U+0432 afii10067 +!E3 U+0433 afii10068 +!E4 U+0434 afii10069 +!E5 U+0435 afii10070 +!E6 U+0436 afii10072 +!E7 U+0437 afii10073 +!E8 U+0438 afii10074 +!E9 U+0439 afii10075 +!EA U+043A afii10076 +!EB U+043B afii10077 +!EC U+043C afii10078 +!ED U+043D afii10079 +!EE U+043E afii10080 +!EF U+043F afii10081 +!F0 U+0440 afii10082 +!F1 U+0441 afii10083 +!F2 U+0442 afii10084 +!F3 U+0443 afii10085 +!F4 U+0444 afii10086 +!F5 U+0445 afii10087 +!F6 U+0446 afii10088 +!F7 U+0447 afii10089 +!F8 U+0448 afii10090 +!F9 U+0449 afii10091 +!FA U+044A afii10092 +!FB U+044B afii10093 +!FC U+044C afii10094 +!FD U+044D afii10095 +!FE U+044E afii10096 +!FF U+044F afii10097 diff --git a/application/third_party/fpdf/makefont/cp1252.map b/application/third_party/fpdf/makefont/cp1252.map new file mode 100644 index 0000000..dd490e5 --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1252.map @@ -0,0 +1,251 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!83 U+0192 florin +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!88 U+02C6 circumflex +!89 U+2030 perthousand +!8A U+0160 Scaron +!8B U+2039 guilsinglleft +!8C U+0152 OE +!8E U+017D Zcaron +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!98 U+02DC tilde +!99 U+2122 trademark +!9A U+0161 scaron +!9B U+203A guilsinglright +!9C U+0153 oe +!9E U+017E zcaron +!9F U+0178 Ydieresis +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+00D0 Eth +!D1 U+00D1 Ntilde +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+00DD Yacute +!DE U+00DE Thorn +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+00F0 eth +!F1 U+00F1 ntilde +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+00FD yacute +!FE U+00FE thorn +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/cp1253.map b/application/third_party/fpdf/makefont/cp1253.map new file mode 100644 index 0000000..4bd826f --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1253.map @@ -0,0 +1,239 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!83 U+0192 florin +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!89 U+2030 perthousand +!8B U+2039 guilsinglleft +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!99 U+2122 trademark +!9B U+203A guilsinglright +!A0 U+00A0 space +!A1 U+0385 dieresistonos +!A2 U+0386 Alphatonos +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+2015 afii00208 +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+0384 tonos +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+0388 Epsilontonos +!B9 U+0389 Etatonos +!BA U+038A Iotatonos +!BB U+00BB guillemotright +!BC U+038C Omicrontonos +!BD U+00BD onehalf +!BE U+038E Upsilontonos +!BF U+038F Omegatonos +!C0 U+0390 iotadieresistonos +!C1 U+0391 Alpha +!C2 U+0392 Beta +!C3 U+0393 Gamma +!C4 U+0394 Delta +!C5 U+0395 Epsilon +!C6 U+0396 Zeta +!C7 U+0397 Eta +!C8 U+0398 Theta +!C9 U+0399 Iota +!CA U+039A Kappa +!CB U+039B Lambda +!CC U+039C Mu +!CD U+039D Nu +!CE U+039E Xi +!CF U+039F Omicron +!D0 U+03A0 Pi +!D1 U+03A1 Rho +!D3 U+03A3 Sigma +!D4 U+03A4 Tau +!D5 U+03A5 Upsilon +!D6 U+03A6 Phi +!D7 U+03A7 Chi +!D8 U+03A8 Psi +!D9 U+03A9 Omega +!DA U+03AA Iotadieresis +!DB U+03AB Upsilondieresis +!DC U+03AC alphatonos +!DD U+03AD epsilontonos +!DE U+03AE etatonos +!DF U+03AF iotatonos +!E0 U+03B0 upsilondieresistonos +!E1 U+03B1 alpha +!E2 U+03B2 beta +!E3 U+03B3 gamma +!E4 U+03B4 delta +!E5 U+03B5 epsilon +!E6 U+03B6 zeta +!E7 U+03B7 eta +!E8 U+03B8 theta +!E9 U+03B9 iota +!EA U+03BA kappa +!EB U+03BB lambda +!EC U+03BC mu +!ED U+03BD nu +!EE U+03BE xi +!EF U+03BF omicron +!F0 U+03C0 pi +!F1 U+03C1 rho +!F2 U+03C2 sigma1 +!F3 U+03C3 sigma +!F4 U+03C4 tau +!F5 U+03C5 upsilon +!F6 U+03C6 phi +!F7 U+03C7 chi +!F8 U+03C8 psi +!F9 U+03C9 omega +!FA U+03CA iotadieresis +!FB U+03CB upsilondieresis +!FC U+03CC omicrontonos +!FD U+03CD upsilontonos +!FE U+03CE omegatonos diff --git a/application/third_party/fpdf/makefont/cp1254.map b/application/third_party/fpdf/makefont/cp1254.map new file mode 100644 index 0000000..829473b --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1254.map @@ -0,0 +1,249 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!83 U+0192 florin +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!88 U+02C6 circumflex +!89 U+2030 perthousand +!8A U+0160 Scaron +!8B U+2039 guilsinglleft +!8C U+0152 OE +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!98 U+02DC tilde +!99 U+2122 trademark +!9A U+0161 scaron +!9B U+203A guilsinglright +!9C U+0153 oe +!9F U+0178 Ydieresis +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+011E Gbreve +!D1 U+00D1 Ntilde +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+0130 Idotaccent +!DE U+015E Scedilla +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+011F gbreve +!F1 U+00F1 ntilde +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+0131 dotlessi +!FE U+015F scedilla +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/cp1255.map b/application/third_party/fpdf/makefont/cp1255.map new file mode 100644 index 0000000..079e10c --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1255.map @@ -0,0 +1,233 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!83 U+0192 florin +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!88 U+02C6 circumflex +!89 U+2030 perthousand +!8B U+2039 guilsinglleft +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!98 U+02DC tilde +!99 U+2122 trademark +!9B U+203A guilsinglright +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+20AA afii57636 +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00D7 multiply +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD sfthyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 middot +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00F7 divide +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+05B0 afii57799 +!C1 U+05B1 afii57801 +!C2 U+05B2 afii57800 +!C3 U+05B3 afii57802 +!C4 U+05B4 afii57793 +!C5 U+05B5 afii57794 +!C6 U+05B6 afii57795 +!C7 U+05B7 afii57798 +!C8 U+05B8 afii57797 +!C9 U+05B9 afii57806 +!CB U+05BB afii57796 +!CC U+05BC afii57807 +!CD U+05BD afii57839 +!CE U+05BE afii57645 +!CF U+05BF afii57841 +!D0 U+05C0 afii57842 +!D1 U+05C1 afii57804 +!D2 U+05C2 afii57803 +!D3 U+05C3 afii57658 +!D4 U+05F0 afii57716 +!D5 U+05F1 afii57717 +!D6 U+05F2 afii57718 +!D7 U+05F3 gereshhebrew +!D8 U+05F4 gershayimhebrew +!E0 U+05D0 afii57664 +!E1 U+05D1 afii57665 +!E2 U+05D2 afii57666 +!E3 U+05D3 afii57667 +!E4 U+05D4 afii57668 +!E5 U+05D5 afii57669 +!E6 U+05D6 afii57670 +!E7 U+05D7 afii57671 +!E8 U+05D8 afii57672 +!E9 U+05D9 afii57673 +!EA U+05DA afii57674 +!EB U+05DB afii57675 +!EC U+05DC afii57676 +!ED U+05DD afii57677 +!EE U+05DE afii57678 +!EF U+05DF afii57679 +!F0 U+05E0 afii57680 +!F1 U+05E1 afii57681 +!F2 U+05E2 afii57682 +!F3 U+05E3 afii57683 +!F4 U+05E4 afii57684 +!F5 U+05E5 afii57685 +!F6 U+05E6 afii57686 +!F7 U+05E7 afii57687 +!F8 U+05E8 afii57688 +!F9 U+05E9 afii57689 +!FA U+05EA afii57690 +!FD U+200E afii299 +!FE U+200F afii300 diff --git a/application/third_party/fpdf/makefont/cp1257.map b/application/third_party/fpdf/makefont/cp1257.map new file mode 100644 index 0000000..2f2ecfa --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1257.map @@ -0,0 +1,244 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!89 U+2030 perthousand +!8B U+2039 guilsinglleft +!8D U+00A8 dieresis +!8E U+02C7 caron +!8F U+00B8 cedilla +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!99 U+2122 trademark +!9B U+203A guilsinglright +!9D U+00AF macron +!9E U+02DB ogonek +!A0 U+00A0 space +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00D8 Oslash +!A9 U+00A9 copyright +!AA U+0156 Rcommaaccent +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00C6 AE +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00F8 oslash +!B9 U+00B9 onesuperior +!BA U+0157 rcommaaccent +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00E6 ae +!C0 U+0104 Aogonek +!C1 U+012E Iogonek +!C2 U+0100 Amacron +!C3 U+0106 Cacute +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+0118 Eogonek +!C7 U+0112 Emacron +!C8 U+010C Ccaron +!C9 U+00C9 Eacute +!CA U+0179 Zacute +!CB U+0116 Edotaccent +!CC U+0122 Gcommaaccent +!CD U+0136 Kcommaaccent +!CE U+012A Imacron +!CF U+013B Lcommaaccent +!D0 U+0160 Scaron +!D1 U+0143 Nacute +!D2 U+0145 Ncommaaccent +!D3 U+00D3 Oacute +!D4 U+014C Omacron +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+0172 Uogonek +!D9 U+0141 Lslash +!DA U+015A Sacute +!DB U+016A Umacron +!DC U+00DC Udieresis +!DD U+017B Zdotaccent +!DE U+017D Zcaron +!DF U+00DF germandbls +!E0 U+0105 aogonek +!E1 U+012F iogonek +!E2 U+0101 amacron +!E3 U+0107 cacute +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+0119 eogonek +!E7 U+0113 emacron +!E8 U+010D ccaron +!E9 U+00E9 eacute +!EA U+017A zacute +!EB U+0117 edotaccent +!EC U+0123 gcommaaccent +!ED U+0137 kcommaaccent +!EE U+012B imacron +!EF U+013C lcommaaccent +!F0 U+0161 scaron +!F1 U+0144 nacute +!F2 U+0146 ncommaaccent +!F3 U+00F3 oacute +!F4 U+014D omacron +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+0173 uogonek +!F9 U+0142 lslash +!FA U+015B sacute +!FB U+016B umacron +!FC U+00FC udieresis +!FD U+017C zdotaccent +!FE U+017E zcaron +!FF U+02D9 dotaccent diff --git a/application/third_party/fpdf/makefont/cp1258.map b/application/third_party/fpdf/makefont/cp1258.map new file mode 100644 index 0000000..fed915f --- /dev/null +++ b/application/third_party/fpdf/makefont/cp1258.map @@ -0,0 +1,247 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!83 U+0192 florin +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!88 U+02C6 circumflex +!89 U+2030 perthousand +!8B U+2039 guilsinglleft +!8C U+0152 OE +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!98 U+02DC tilde +!99 U+2122 trademark +!9B U+203A guilsinglright +!9C U+0153 oe +!9F U+0178 Ydieresis +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+0102 Abreve +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+0300 gravecomb +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+0110 Dcroat +!D1 U+00D1 Ntilde +!D2 U+0309 hookabovecomb +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+01A0 Ohorn +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+01AF Uhorn +!DE U+0303 tildecomb +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+0103 abreve +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+0301 acutecomb +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+0111 dcroat +!F1 U+00F1 ntilde +!F2 U+0323 dotbelowcomb +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+01A1 ohorn +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+01B0 uhorn +!FE U+20AB dong +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/cp874.map b/application/third_party/fpdf/makefont/cp874.map new file mode 100644 index 0000000..1006e6b --- /dev/null +++ b/application/third_party/fpdf/makefont/cp874.map @@ -0,0 +1,225 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!85 U+2026 ellipsis +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!A0 U+00A0 space +!A1 U+0E01 kokaithai +!A2 U+0E02 khokhaithai +!A3 U+0E03 khokhuatthai +!A4 U+0E04 khokhwaithai +!A5 U+0E05 khokhonthai +!A6 U+0E06 khorakhangthai +!A7 U+0E07 ngonguthai +!A8 U+0E08 chochanthai +!A9 U+0E09 chochingthai +!AA U+0E0A chochangthai +!AB U+0E0B sosothai +!AC U+0E0C chochoethai +!AD U+0E0D yoyingthai +!AE U+0E0E dochadathai +!AF U+0E0F topatakthai +!B0 U+0E10 thothanthai +!B1 U+0E11 thonangmonthothai +!B2 U+0E12 thophuthaothai +!B3 U+0E13 nonenthai +!B4 U+0E14 dodekthai +!B5 U+0E15 totaothai +!B6 U+0E16 thothungthai +!B7 U+0E17 thothahanthai +!B8 U+0E18 thothongthai +!B9 U+0E19 nonuthai +!BA U+0E1A bobaimaithai +!BB U+0E1B poplathai +!BC U+0E1C phophungthai +!BD U+0E1D fofathai +!BE U+0E1E phophanthai +!BF U+0E1F fofanthai +!C0 U+0E20 phosamphaothai +!C1 U+0E21 momathai +!C2 U+0E22 yoyakthai +!C3 U+0E23 roruathai +!C4 U+0E24 ruthai +!C5 U+0E25 lolingthai +!C6 U+0E26 luthai +!C7 U+0E27 wowaenthai +!C8 U+0E28 sosalathai +!C9 U+0E29 sorusithai +!CA U+0E2A sosuathai +!CB U+0E2B hohipthai +!CC U+0E2C lochulathai +!CD U+0E2D oangthai +!CE U+0E2E honokhukthai +!CF U+0E2F paiyannoithai +!D0 U+0E30 saraathai +!D1 U+0E31 maihanakatthai +!D2 U+0E32 saraaathai +!D3 U+0E33 saraamthai +!D4 U+0E34 saraithai +!D5 U+0E35 saraiithai +!D6 U+0E36 sarauethai +!D7 U+0E37 saraueethai +!D8 U+0E38 sarauthai +!D9 U+0E39 sarauuthai +!DA U+0E3A phinthuthai +!DF U+0E3F bahtthai +!E0 U+0E40 saraethai +!E1 U+0E41 saraaethai +!E2 U+0E42 saraothai +!E3 U+0E43 saraaimaimuanthai +!E4 U+0E44 saraaimaimalaithai +!E5 U+0E45 lakkhangyaothai +!E6 U+0E46 maiyamokthai +!E7 U+0E47 maitaikhuthai +!E8 U+0E48 maiekthai +!E9 U+0E49 maithothai +!EA U+0E4A maitrithai +!EB U+0E4B maichattawathai +!EC U+0E4C thanthakhatthai +!ED U+0E4D nikhahitthai +!EE U+0E4E yamakkanthai +!EF U+0E4F fongmanthai +!F0 U+0E50 zerothai +!F1 U+0E51 onethai +!F2 U+0E52 twothai +!F3 U+0E53 threethai +!F4 U+0E54 fourthai +!F5 U+0E55 fivethai +!F6 U+0E56 sixthai +!F7 U+0E57 seventhai +!F8 U+0E58 eightthai +!F9 U+0E59 ninethai +!FA U+0E5A angkhankhuthai +!FB U+0E5B khomutthai diff --git a/application/third_party/fpdf/makefont/iso-8859-1.map b/application/third_party/fpdf/makefont/iso-8859-1.map new file mode 100644 index 0000000..61740a3 --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-1.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+00D0 Eth +!D1 U+00D1 Ntilde +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+00DD Yacute +!DE U+00DE Thorn +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+00F0 eth +!F1 U+00F1 ntilde +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+00FD yacute +!FE U+00FE thorn +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/iso-8859-11.map b/application/third_party/fpdf/makefont/iso-8859-11.map new file mode 100644 index 0000000..9168812 --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-11.map @@ -0,0 +1,248 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+0E01 kokaithai +!A2 U+0E02 khokhaithai +!A3 U+0E03 khokhuatthai +!A4 U+0E04 khokhwaithai +!A5 U+0E05 khokhonthai +!A6 U+0E06 khorakhangthai +!A7 U+0E07 ngonguthai +!A8 U+0E08 chochanthai +!A9 U+0E09 chochingthai +!AA U+0E0A chochangthai +!AB U+0E0B sosothai +!AC U+0E0C chochoethai +!AD U+0E0D yoyingthai +!AE U+0E0E dochadathai +!AF U+0E0F topatakthai +!B0 U+0E10 thothanthai +!B1 U+0E11 thonangmonthothai +!B2 U+0E12 thophuthaothai +!B3 U+0E13 nonenthai +!B4 U+0E14 dodekthai +!B5 U+0E15 totaothai +!B6 U+0E16 thothungthai +!B7 U+0E17 thothahanthai +!B8 U+0E18 thothongthai +!B9 U+0E19 nonuthai +!BA U+0E1A bobaimaithai +!BB U+0E1B poplathai +!BC U+0E1C phophungthai +!BD U+0E1D fofathai +!BE U+0E1E phophanthai +!BF U+0E1F fofanthai +!C0 U+0E20 phosamphaothai +!C1 U+0E21 momathai +!C2 U+0E22 yoyakthai +!C3 U+0E23 roruathai +!C4 U+0E24 ruthai +!C5 U+0E25 lolingthai +!C6 U+0E26 luthai +!C7 U+0E27 wowaenthai +!C8 U+0E28 sosalathai +!C9 U+0E29 sorusithai +!CA U+0E2A sosuathai +!CB U+0E2B hohipthai +!CC U+0E2C lochulathai +!CD U+0E2D oangthai +!CE U+0E2E honokhukthai +!CF U+0E2F paiyannoithai +!D0 U+0E30 saraathai +!D1 U+0E31 maihanakatthai +!D2 U+0E32 saraaathai +!D3 U+0E33 saraamthai +!D4 U+0E34 saraithai +!D5 U+0E35 saraiithai +!D6 U+0E36 sarauethai +!D7 U+0E37 saraueethai +!D8 U+0E38 sarauthai +!D9 U+0E39 sarauuthai +!DA U+0E3A phinthuthai +!DF U+0E3F bahtthai +!E0 U+0E40 saraethai +!E1 U+0E41 saraaethai +!E2 U+0E42 saraothai +!E3 U+0E43 saraaimaimuanthai +!E4 U+0E44 saraaimaimalaithai +!E5 U+0E45 lakkhangyaothai +!E6 U+0E46 maiyamokthai +!E7 U+0E47 maitaikhuthai +!E8 U+0E48 maiekthai +!E9 U+0E49 maithothai +!EA U+0E4A maitrithai +!EB U+0E4B maichattawathai +!EC U+0E4C thanthakhatthai +!ED U+0E4D nikhahitthai +!EE U+0E4E yamakkanthai +!EF U+0E4F fongmanthai +!F0 U+0E50 zerothai +!F1 U+0E51 onethai +!F2 U+0E52 twothai +!F3 U+0E53 threethai +!F4 U+0E54 fourthai +!F5 U+0E55 fivethai +!F6 U+0E56 sixthai +!F7 U+0E57 seventhai +!F8 U+0E58 eightthai +!F9 U+0E59 ninethai +!FA U+0E5A angkhankhuthai +!FB U+0E5B khomutthai diff --git a/application/third_party/fpdf/makefont/iso-8859-15.map b/application/third_party/fpdf/makefont/iso-8859-15.map new file mode 100644 index 0000000..6c2b571 --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-15.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+20AC Euro +!A5 U+00A5 yen +!A6 U+0160 Scaron +!A7 U+00A7 section +!A8 U+0161 scaron +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+017D Zcaron +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+017E zcaron +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+0152 OE +!BD U+0153 oe +!BE U+0178 Ydieresis +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+00D0 Eth +!D1 U+00D1 Ntilde +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+00DD Yacute +!DE U+00DE Thorn +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+00F0 eth +!F1 U+00F1 ntilde +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+00FD yacute +!FE U+00FE thorn +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/iso-8859-16.map b/application/third_party/fpdf/makefont/iso-8859-16.map new file mode 100644 index 0000000..202c8fe --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-16.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+0104 Aogonek +!A2 U+0105 aogonek +!A3 U+0141 Lslash +!A4 U+20AC Euro +!A5 U+201E quotedblbase +!A6 U+0160 Scaron +!A7 U+00A7 section +!A8 U+0161 scaron +!A9 U+00A9 copyright +!AA U+0218 Scommaaccent +!AB U+00AB guillemotleft +!AC U+0179 Zacute +!AD U+00AD hyphen +!AE U+017A zacute +!AF U+017B Zdotaccent +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+010C Ccaron +!B3 U+0142 lslash +!B4 U+017D Zcaron +!B5 U+201D quotedblright +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+017E zcaron +!B9 U+010D ccaron +!BA U+0219 scommaaccent +!BB U+00BB guillemotright +!BC U+0152 OE +!BD U+0153 oe +!BE U+0178 Ydieresis +!BF U+017C zdotaccent +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+0102 Abreve +!C4 U+00C4 Adieresis +!C5 U+0106 Cacute +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+0110 Dcroat +!D1 U+0143 Nacute +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+0150 Ohungarumlaut +!D6 U+00D6 Odieresis +!D7 U+015A Sacute +!D8 U+0170 Uhungarumlaut +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+0118 Eogonek +!DE U+021A Tcommaaccent +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+0103 abreve +!E4 U+00E4 adieresis +!E5 U+0107 cacute +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+0111 dcroat +!F1 U+0144 nacute +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+0151 ohungarumlaut +!F6 U+00F6 odieresis +!F7 U+015B sacute +!F8 U+0171 uhungarumlaut +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+0119 eogonek +!FE U+021B tcommaaccent +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/iso-8859-2.map b/application/third_party/fpdf/makefont/iso-8859-2.map new file mode 100644 index 0000000..65ae09f --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-2.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+0104 Aogonek +!A2 U+02D8 breve +!A3 U+0141 Lslash +!A4 U+00A4 currency +!A5 U+013D Lcaron +!A6 U+015A Sacute +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+0160 Scaron +!AA U+015E Scedilla +!AB U+0164 Tcaron +!AC U+0179 Zacute +!AD U+00AD hyphen +!AE U+017D Zcaron +!AF U+017B Zdotaccent +!B0 U+00B0 degree +!B1 U+0105 aogonek +!B2 U+02DB ogonek +!B3 U+0142 lslash +!B4 U+00B4 acute +!B5 U+013E lcaron +!B6 U+015B sacute +!B7 U+02C7 caron +!B8 U+00B8 cedilla +!B9 U+0161 scaron +!BA U+015F scedilla +!BB U+0165 tcaron +!BC U+017A zacute +!BD U+02DD hungarumlaut +!BE U+017E zcaron +!BF U+017C zdotaccent +!C0 U+0154 Racute +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+0102 Abreve +!C4 U+00C4 Adieresis +!C5 U+0139 Lacute +!C6 U+0106 Cacute +!C7 U+00C7 Ccedilla +!C8 U+010C Ccaron +!C9 U+00C9 Eacute +!CA U+0118 Eogonek +!CB U+00CB Edieresis +!CC U+011A Ecaron +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+010E Dcaron +!D0 U+0110 Dcroat +!D1 U+0143 Nacute +!D2 U+0147 Ncaron +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+0150 Ohungarumlaut +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+0158 Rcaron +!D9 U+016E Uring +!DA U+00DA Uacute +!DB U+0170 Uhungarumlaut +!DC U+00DC Udieresis +!DD U+00DD Yacute +!DE U+0162 Tcommaaccent +!DF U+00DF germandbls +!E0 U+0155 racute +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+0103 abreve +!E4 U+00E4 adieresis +!E5 U+013A lacute +!E6 U+0107 cacute +!E7 U+00E7 ccedilla +!E8 U+010D ccaron +!E9 U+00E9 eacute +!EA U+0119 eogonek +!EB U+00EB edieresis +!EC U+011B ecaron +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+010F dcaron +!F0 U+0111 dcroat +!F1 U+0144 nacute +!F2 U+0148 ncaron +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+0151 ohungarumlaut +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+0159 rcaron +!F9 U+016F uring +!FA U+00FA uacute +!FB U+0171 uhungarumlaut +!FC U+00FC udieresis +!FD U+00FD yacute +!FE U+0163 tcommaaccent +!FF U+02D9 dotaccent diff --git a/application/third_party/fpdf/makefont/iso-8859-4.map b/application/third_party/fpdf/makefont/iso-8859-4.map new file mode 100644 index 0000000..a7d87bf --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-4.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+0104 Aogonek +!A2 U+0138 kgreenlandic +!A3 U+0156 Rcommaaccent +!A4 U+00A4 currency +!A5 U+0128 Itilde +!A6 U+013B Lcommaaccent +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+0160 Scaron +!AA U+0112 Emacron +!AB U+0122 Gcommaaccent +!AC U+0166 Tbar +!AD U+00AD hyphen +!AE U+017D Zcaron +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+0105 aogonek +!B2 U+02DB ogonek +!B3 U+0157 rcommaaccent +!B4 U+00B4 acute +!B5 U+0129 itilde +!B6 U+013C lcommaaccent +!B7 U+02C7 caron +!B8 U+00B8 cedilla +!B9 U+0161 scaron +!BA U+0113 emacron +!BB U+0123 gcommaaccent +!BC U+0167 tbar +!BD U+014A Eng +!BE U+017E zcaron +!BF U+014B eng +!C0 U+0100 Amacron +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+012E Iogonek +!C8 U+010C Ccaron +!C9 U+00C9 Eacute +!CA U+0118 Eogonek +!CB U+00CB Edieresis +!CC U+0116 Edotaccent +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+012A Imacron +!D0 U+0110 Dcroat +!D1 U+0145 Ncommaaccent +!D2 U+014C Omacron +!D3 U+0136 Kcommaaccent +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+0172 Uogonek +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+0168 Utilde +!DE U+016A Umacron +!DF U+00DF germandbls +!E0 U+0101 amacron +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+012F iogonek +!E8 U+010D ccaron +!E9 U+00E9 eacute +!EA U+0119 eogonek +!EB U+00EB edieresis +!EC U+0117 edotaccent +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+012B imacron +!F0 U+0111 dcroat +!F1 U+0146 ncommaaccent +!F2 U+014D omacron +!F3 U+0137 kcommaaccent +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+0173 uogonek +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+0169 utilde +!FE U+016B umacron +!FF U+02D9 dotaccent diff --git a/application/third_party/fpdf/makefont/iso-8859-5.map b/application/third_party/fpdf/makefont/iso-8859-5.map new file mode 100644 index 0000000..f9cd4ed --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-5.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+0401 afii10023 +!A2 U+0402 afii10051 +!A3 U+0403 afii10052 +!A4 U+0404 afii10053 +!A5 U+0405 afii10054 +!A6 U+0406 afii10055 +!A7 U+0407 afii10056 +!A8 U+0408 afii10057 +!A9 U+0409 afii10058 +!AA U+040A afii10059 +!AB U+040B afii10060 +!AC U+040C afii10061 +!AD U+00AD hyphen +!AE U+040E afii10062 +!AF U+040F afii10145 +!B0 U+0410 afii10017 +!B1 U+0411 afii10018 +!B2 U+0412 afii10019 +!B3 U+0413 afii10020 +!B4 U+0414 afii10021 +!B5 U+0415 afii10022 +!B6 U+0416 afii10024 +!B7 U+0417 afii10025 +!B8 U+0418 afii10026 +!B9 U+0419 afii10027 +!BA U+041A afii10028 +!BB U+041B afii10029 +!BC U+041C afii10030 +!BD U+041D afii10031 +!BE U+041E afii10032 +!BF U+041F afii10033 +!C0 U+0420 afii10034 +!C1 U+0421 afii10035 +!C2 U+0422 afii10036 +!C3 U+0423 afii10037 +!C4 U+0424 afii10038 +!C5 U+0425 afii10039 +!C6 U+0426 afii10040 +!C7 U+0427 afii10041 +!C8 U+0428 afii10042 +!C9 U+0429 afii10043 +!CA U+042A afii10044 +!CB U+042B afii10045 +!CC U+042C afii10046 +!CD U+042D afii10047 +!CE U+042E afii10048 +!CF U+042F afii10049 +!D0 U+0430 afii10065 +!D1 U+0431 afii10066 +!D2 U+0432 afii10067 +!D3 U+0433 afii10068 +!D4 U+0434 afii10069 +!D5 U+0435 afii10070 +!D6 U+0436 afii10072 +!D7 U+0437 afii10073 +!D8 U+0438 afii10074 +!D9 U+0439 afii10075 +!DA U+043A afii10076 +!DB U+043B afii10077 +!DC U+043C afii10078 +!DD U+043D afii10079 +!DE U+043E afii10080 +!DF U+043F afii10081 +!E0 U+0440 afii10082 +!E1 U+0441 afii10083 +!E2 U+0442 afii10084 +!E3 U+0443 afii10085 +!E4 U+0444 afii10086 +!E5 U+0445 afii10087 +!E6 U+0446 afii10088 +!E7 U+0447 afii10089 +!E8 U+0448 afii10090 +!E9 U+0449 afii10091 +!EA U+044A afii10092 +!EB U+044B afii10093 +!EC U+044C afii10094 +!ED U+044D afii10095 +!EE U+044E afii10096 +!EF U+044F afii10097 +!F0 U+2116 afii61352 +!F1 U+0451 afii10071 +!F2 U+0452 afii10099 +!F3 U+0453 afii10100 +!F4 U+0454 afii10101 +!F5 U+0455 afii10102 +!F6 U+0456 afii10103 +!F7 U+0457 afii10104 +!F8 U+0458 afii10105 +!F9 U+0459 afii10106 +!FA U+045A afii10107 +!FB U+045B afii10108 +!FC U+045C afii10109 +!FD U+00A7 section +!FE U+045E afii10110 +!FF U+045F afii10193 diff --git a/application/third_party/fpdf/makefont/iso-8859-7.map b/application/third_party/fpdf/makefont/iso-8859-7.map new file mode 100644 index 0000000..e163796 --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-7.map @@ -0,0 +1,250 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+2018 quoteleft +!A2 U+2019 quoteright +!A3 U+00A3 sterling +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AF U+2015 afii00208 +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+0384 tonos +!B5 U+0385 dieresistonos +!B6 U+0386 Alphatonos +!B7 U+00B7 periodcentered +!B8 U+0388 Epsilontonos +!B9 U+0389 Etatonos +!BA U+038A Iotatonos +!BB U+00BB guillemotright +!BC U+038C Omicrontonos +!BD U+00BD onehalf +!BE U+038E Upsilontonos +!BF U+038F Omegatonos +!C0 U+0390 iotadieresistonos +!C1 U+0391 Alpha +!C2 U+0392 Beta +!C3 U+0393 Gamma +!C4 U+0394 Delta +!C5 U+0395 Epsilon +!C6 U+0396 Zeta +!C7 U+0397 Eta +!C8 U+0398 Theta +!C9 U+0399 Iota +!CA U+039A Kappa +!CB U+039B Lambda +!CC U+039C Mu +!CD U+039D Nu +!CE U+039E Xi +!CF U+039F Omicron +!D0 U+03A0 Pi +!D1 U+03A1 Rho +!D3 U+03A3 Sigma +!D4 U+03A4 Tau +!D5 U+03A5 Upsilon +!D6 U+03A6 Phi +!D7 U+03A7 Chi +!D8 U+03A8 Psi +!D9 U+03A9 Omega +!DA U+03AA Iotadieresis +!DB U+03AB Upsilondieresis +!DC U+03AC alphatonos +!DD U+03AD epsilontonos +!DE U+03AE etatonos +!DF U+03AF iotatonos +!E0 U+03B0 upsilondieresistonos +!E1 U+03B1 alpha +!E2 U+03B2 beta +!E3 U+03B3 gamma +!E4 U+03B4 delta +!E5 U+03B5 epsilon +!E6 U+03B6 zeta +!E7 U+03B7 eta +!E8 U+03B8 theta +!E9 U+03B9 iota +!EA U+03BA kappa +!EB U+03BB lambda +!EC U+03BC mu +!ED U+03BD nu +!EE U+03BE xi +!EF U+03BF omicron +!F0 U+03C0 pi +!F1 U+03C1 rho +!F2 U+03C2 sigma1 +!F3 U+03C3 sigma +!F4 U+03C4 tau +!F5 U+03C5 upsilon +!F6 U+03C6 phi +!F7 U+03C7 chi +!F8 U+03C8 psi +!F9 U+03C9 omega +!FA U+03CA iotadieresis +!FB U+03CB upsilondieresis +!FC U+03CC omicrontonos +!FD U+03CD upsilontonos +!FE U+03CE omegatonos diff --git a/application/third_party/fpdf/makefont/iso-8859-9.map b/application/third_party/fpdf/makefont/iso-8859-9.map new file mode 100644 index 0000000..48c123a --- /dev/null +++ b/application/third_party/fpdf/makefont/iso-8859-9.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+0080 .notdef +!81 U+0081 .notdef +!82 U+0082 .notdef +!83 U+0083 .notdef +!84 U+0084 .notdef +!85 U+0085 .notdef +!86 U+0086 .notdef +!87 U+0087 .notdef +!88 U+0088 .notdef +!89 U+0089 .notdef +!8A U+008A .notdef +!8B U+008B .notdef +!8C U+008C .notdef +!8D U+008D .notdef +!8E U+008E .notdef +!8F U+008F .notdef +!90 U+0090 .notdef +!91 U+0091 .notdef +!92 U+0092 .notdef +!93 U+0093 .notdef +!94 U+0094 .notdef +!95 U+0095 .notdef +!96 U+0096 .notdef +!97 U+0097 .notdef +!98 U+0098 .notdef +!99 U+0099 .notdef +!9A U+009A .notdef +!9B U+009B .notdef +!9C U+009C .notdef +!9D U+009D .notdef +!9E U+009E .notdef +!9F U+009F .notdef +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+011E Gbreve +!D1 U+00D1 Ntilde +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+0130 Idotaccent +!DE U+015E Scedilla +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+011F gbreve +!F1 U+00F1 ntilde +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+0131 dotlessi +!FE U+015F scedilla +!FF U+00FF ydieresis diff --git a/application/third_party/fpdf/makefont/koi8-r.map b/application/third_party/fpdf/makefont/koi8-r.map new file mode 100644 index 0000000..6ad5d05 --- /dev/null +++ b/application/third_party/fpdf/makefont/koi8-r.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+2500 SF100000 +!81 U+2502 SF110000 +!82 U+250C SF010000 +!83 U+2510 SF030000 +!84 U+2514 SF020000 +!85 U+2518 SF040000 +!86 U+251C SF080000 +!87 U+2524 SF090000 +!88 U+252C SF060000 +!89 U+2534 SF070000 +!8A U+253C SF050000 +!8B U+2580 upblock +!8C U+2584 dnblock +!8D U+2588 block +!8E U+258C lfblock +!8F U+2590 rtblock +!90 U+2591 ltshade +!91 U+2592 shade +!92 U+2593 dkshade +!93 U+2320 integraltp +!94 U+25A0 filledbox +!95 U+2219 periodcentered +!96 U+221A radical +!97 U+2248 approxequal +!98 U+2264 lessequal +!99 U+2265 greaterequal +!9A U+00A0 space +!9B U+2321 integralbt +!9C U+00B0 degree +!9D U+00B2 twosuperior +!9E U+00B7 periodcentered +!9F U+00F7 divide +!A0 U+2550 SF430000 +!A1 U+2551 SF240000 +!A2 U+2552 SF510000 +!A3 U+0451 afii10071 +!A4 U+2553 SF520000 +!A5 U+2554 SF390000 +!A6 U+2555 SF220000 +!A7 U+2556 SF210000 +!A8 U+2557 SF250000 +!A9 U+2558 SF500000 +!AA U+2559 SF490000 +!AB U+255A SF380000 +!AC U+255B SF280000 +!AD U+255C SF270000 +!AE U+255D SF260000 +!AF U+255E SF360000 +!B0 U+255F SF370000 +!B1 U+2560 SF420000 +!B2 U+2561 SF190000 +!B3 U+0401 afii10023 +!B4 U+2562 SF200000 +!B5 U+2563 SF230000 +!B6 U+2564 SF470000 +!B7 U+2565 SF480000 +!B8 U+2566 SF410000 +!B9 U+2567 SF450000 +!BA U+2568 SF460000 +!BB U+2569 SF400000 +!BC U+256A SF540000 +!BD U+256B SF530000 +!BE U+256C SF440000 +!BF U+00A9 copyright +!C0 U+044E afii10096 +!C1 U+0430 afii10065 +!C2 U+0431 afii10066 +!C3 U+0446 afii10088 +!C4 U+0434 afii10069 +!C5 U+0435 afii10070 +!C6 U+0444 afii10086 +!C7 U+0433 afii10068 +!C8 U+0445 afii10087 +!C9 U+0438 afii10074 +!CA U+0439 afii10075 +!CB U+043A afii10076 +!CC U+043B afii10077 +!CD U+043C afii10078 +!CE U+043D afii10079 +!CF U+043E afii10080 +!D0 U+043F afii10081 +!D1 U+044F afii10097 +!D2 U+0440 afii10082 +!D3 U+0441 afii10083 +!D4 U+0442 afii10084 +!D5 U+0443 afii10085 +!D6 U+0436 afii10072 +!D7 U+0432 afii10067 +!D8 U+044C afii10094 +!D9 U+044B afii10093 +!DA U+0437 afii10073 +!DB U+0448 afii10090 +!DC U+044D afii10095 +!DD U+0449 afii10091 +!DE U+0447 afii10089 +!DF U+044A afii10092 +!E0 U+042E afii10048 +!E1 U+0410 afii10017 +!E2 U+0411 afii10018 +!E3 U+0426 afii10040 +!E4 U+0414 afii10021 +!E5 U+0415 afii10022 +!E6 U+0424 afii10038 +!E7 U+0413 afii10020 +!E8 U+0425 afii10039 +!E9 U+0418 afii10026 +!EA U+0419 afii10027 +!EB U+041A afii10028 +!EC U+041B afii10029 +!ED U+041C afii10030 +!EE U+041D afii10031 +!EF U+041E afii10032 +!F0 U+041F afii10033 +!F1 U+042F afii10049 +!F2 U+0420 afii10034 +!F3 U+0421 afii10035 +!F4 U+0422 afii10036 +!F5 U+0423 afii10037 +!F6 U+0416 afii10024 +!F7 U+0412 afii10019 +!F8 U+042C afii10046 +!F9 U+042B afii10045 +!FA U+0417 afii10025 +!FB U+0428 afii10042 +!FC U+042D afii10047 +!FD U+0429 afii10043 +!FE U+0427 afii10041 +!FF U+042A afii10044 diff --git a/application/third_party/fpdf/makefont/koi8-u.map b/application/third_party/fpdf/makefont/koi8-u.map new file mode 100644 index 0000000..40a7e4f --- /dev/null +++ b/application/third_party/fpdf/makefont/koi8-u.map @@ -0,0 +1,256 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+2500 SF100000 +!81 U+2502 SF110000 +!82 U+250C SF010000 +!83 U+2510 SF030000 +!84 U+2514 SF020000 +!85 U+2518 SF040000 +!86 U+251C SF080000 +!87 U+2524 SF090000 +!88 U+252C SF060000 +!89 U+2534 SF070000 +!8A U+253C SF050000 +!8B U+2580 upblock +!8C U+2584 dnblock +!8D U+2588 block +!8E U+258C lfblock +!8F U+2590 rtblock +!90 U+2591 ltshade +!91 U+2592 shade +!92 U+2593 dkshade +!93 U+2320 integraltp +!94 U+25A0 filledbox +!95 U+2022 bullet +!96 U+221A radical +!97 U+2248 approxequal +!98 U+2264 lessequal +!99 U+2265 greaterequal +!9A U+00A0 space +!9B U+2321 integralbt +!9C U+00B0 degree +!9D U+00B2 twosuperior +!9E U+00B7 periodcentered +!9F U+00F7 divide +!A0 U+2550 SF430000 +!A1 U+2551 SF240000 +!A2 U+2552 SF510000 +!A3 U+0451 afii10071 +!A4 U+0454 afii10101 +!A5 U+2554 SF390000 +!A6 U+0456 afii10103 +!A7 U+0457 afii10104 +!A8 U+2557 SF250000 +!A9 U+2558 SF500000 +!AA U+2559 SF490000 +!AB U+255A SF380000 +!AC U+255B SF280000 +!AD U+0491 afii10098 +!AE U+255D SF260000 +!AF U+255E SF360000 +!B0 U+255F SF370000 +!B1 U+2560 SF420000 +!B2 U+2561 SF190000 +!B3 U+0401 afii10023 +!B4 U+0404 afii10053 +!B5 U+2563 SF230000 +!B6 U+0406 afii10055 +!B7 U+0407 afii10056 +!B8 U+2566 SF410000 +!B9 U+2567 SF450000 +!BA U+2568 SF460000 +!BB U+2569 SF400000 +!BC U+256A SF540000 +!BD U+0490 afii10050 +!BE U+256C SF440000 +!BF U+00A9 copyright +!C0 U+044E afii10096 +!C1 U+0430 afii10065 +!C2 U+0431 afii10066 +!C3 U+0446 afii10088 +!C4 U+0434 afii10069 +!C5 U+0435 afii10070 +!C6 U+0444 afii10086 +!C7 U+0433 afii10068 +!C8 U+0445 afii10087 +!C9 U+0438 afii10074 +!CA U+0439 afii10075 +!CB U+043A afii10076 +!CC U+043B afii10077 +!CD U+043C afii10078 +!CE U+043D afii10079 +!CF U+043E afii10080 +!D0 U+043F afii10081 +!D1 U+044F afii10097 +!D2 U+0440 afii10082 +!D3 U+0441 afii10083 +!D4 U+0442 afii10084 +!D5 U+0443 afii10085 +!D6 U+0436 afii10072 +!D7 U+0432 afii10067 +!D8 U+044C afii10094 +!D9 U+044B afii10093 +!DA U+0437 afii10073 +!DB U+0448 afii10090 +!DC U+044D afii10095 +!DD U+0449 afii10091 +!DE U+0447 afii10089 +!DF U+044A afii10092 +!E0 U+042E afii10048 +!E1 U+0410 afii10017 +!E2 U+0411 afii10018 +!E3 U+0426 afii10040 +!E4 U+0414 afii10021 +!E5 U+0415 afii10022 +!E6 U+0424 afii10038 +!E7 U+0413 afii10020 +!E8 U+0425 afii10039 +!E9 U+0418 afii10026 +!EA U+0419 afii10027 +!EB U+041A afii10028 +!EC U+041B afii10029 +!ED U+041C afii10030 +!EE U+041D afii10031 +!EF U+041E afii10032 +!F0 U+041F afii10033 +!F1 U+042F afii10049 +!F2 U+0420 afii10034 +!F3 U+0421 afii10035 +!F4 U+0422 afii10036 +!F5 U+0423 afii10037 +!F6 U+0416 afii10024 +!F7 U+0412 afii10019 +!F8 U+042C afii10046 +!F9 U+042B afii10045 +!FA U+0417 afii10025 +!FB U+0428 afii10042 +!FC U+042D afii10047 +!FD U+0429 afii10043 +!FE U+0427 afii10041 +!FF U+042A afii10044 diff --git a/application/third_party/fpdf/makefont/makefont.php b/application/third_party/fpdf/makefont/makefont.php new file mode 100644 index 0000000..fbe8dcf --- /dev/null +++ b/application/third_party/fpdf/makefont/makefont.php @@ -0,0 +1,447 @@ +$severity: "; + echo "$txt
"; + } +} + +function Notice($txt) +{ + Message($txt, 'Notice'); +} + +function Warning($txt) +{ + Message($txt, 'Warning'); +} + +function Error($txt) +{ + Message($txt, 'Error'); + exit; +} + +function LoadMap($enc) +{ + $file = dirname(__FILE__).'/'.strtolower($enc).'.map'; + $a = file($file); + if(empty($a)) + Error('Encoding not found: '.$enc); + $map = array_fill(0, 256, array('uv'=>-1, 'name'=>'.notdef')); + foreach($a as $line) + { + $e = explode(' ', rtrim($line)); + $c = hexdec(substr($e[0],1)); + $uv = hexdec(substr($e[1],2)); + $name = $e[2]; + $map[$c] = array('uv'=>$uv, 'name'=>$name); + } + return $map; +} + +function GetInfoFromTrueType($file, $embed, $subset, $map) +{ + // Return information from a TrueType font + try + { + $ttf = new TTFParser($file); + $ttf->Parse(); + } + catch(Exception $e) + { + Error($e->getMessage()); + } + if($embed) + { + if(!$ttf->embeddable) + Error('Font license does not allow embedding'); + if($subset) + { + $chars = array(); + foreach($map as $v) + { + if($v['name']!='.notdef') + $chars[] = $v['uv']; + } + $ttf->Subset($chars); + $info['Data'] = $ttf->Build(); + } + else + $info['Data'] = file_get_contents($file); + $info['OriginalSize'] = strlen($info['Data']); + } + $k = 1000/$ttf->unitsPerEm; + $info['FontName'] = $ttf->postScriptName; + $info['Bold'] = $ttf->bold; + $info['ItalicAngle'] = $ttf->italicAngle; + $info['IsFixedPitch'] = $ttf->isFixedPitch; + $info['Ascender'] = round($k*$ttf->typoAscender); + $info['Descender'] = round($k*$ttf->typoDescender); + $info['UnderlineThickness'] = round($k*$ttf->underlineThickness); + $info['UnderlinePosition'] = round($k*$ttf->underlinePosition); + $info['FontBBox'] = array(round($k*$ttf->xMin), round($k*$ttf->yMin), round($k*$ttf->xMax), round($k*$ttf->yMax)); + $info['CapHeight'] = round($k*$ttf->capHeight); + $info['MissingWidth'] = round($k*$ttf->glyphs[0]['w']); + $widths = array_fill(0, 256, $info['MissingWidth']); + foreach($map as $c=>$v) + { + if($v['name']!='.notdef') + { + if(isset($ttf->chars[$v['uv']])) + { + $id = $ttf->chars[$v['uv']]; + $w = $ttf->glyphs[$id]['w']; + $widths[$c] = round($k*$w); + } + else + Warning('Character '.$v['name'].' is missing'); + } + } + $info['Widths'] = $widths; + return $info; +} + +function GetInfoFromType1($file, $embed, $map) +{ + // Return information from a Type1 font + if($embed) + { + $f = fopen($file, 'rb'); + if(!$f) + Error('Can\'t open font file'); + // Read first segment + $a = unpack('Cmarker/Ctype/Vsize', fread($f,6)); + if($a['marker']!=128) + Error('Font file is not a valid binary Type1'); + $size1 = $a['size']; + $data = fread($f, $size1); + // Read second segment + $a = unpack('Cmarker/Ctype/Vsize', fread($f,6)); + if($a['marker']!=128) + Error('Font file is not a valid binary Type1'); + $size2 = $a['size']; + $data .= fread($f, $size2); + fclose($f); + $info['Data'] = $data; + $info['Size1'] = $size1; + $info['Size2'] = $size2; + } + + $afm = substr($file, 0, -3).'afm'; + if(!file_exists($afm)) + Error('AFM font file not found: '.$afm); + $a = file($afm); + if(empty($a)) + Error('AFM file empty or not readable'); + foreach($a as $line) + { + $e = explode(' ', rtrim($line)); + if(count($e)<2) + continue; + $entry = $e[0]; + if($entry=='C') + { + $w = $e[4]; + $name = $e[7]; + $cw[$name] = $w; + } + elseif($entry=='FontName') + $info['FontName'] = $e[1]; + elseif($entry=='Weight') + $info['Weight'] = $e[1]; + elseif($entry=='ItalicAngle') + $info['ItalicAngle'] = (int)$e[1]; + elseif($entry=='Ascender') + $info['Ascender'] = (int)$e[1]; + elseif($entry=='Descender') + $info['Descender'] = (int)$e[1]; + elseif($entry=='UnderlineThickness') + $info['UnderlineThickness'] = (int)$e[1]; + elseif($entry=='UnderlinePosition') + $info['UnderlinePosition'] = (int)$e[1]; + elseif($entry=='IsFixedPitch') + $info['IsFixedPitch'] = ($e[1]=='true'); + elseif($entry=='FontBBox') + $info['FontBBox'] = array((int)$e[1], (int)$e[2], (int)$e[3], (int)$e[4]); + elseif($entry=='CapHeight') + $info['CapHeight'] = (int)$e[1]; + elseif($entry=='StdVW') + $info['StdVW'] = (int)$e[1]; + } + + if(!isset($info['FontName'])) + Error('FontName missing in AFM file'); + if(!isset($info['Ascender'])) + $info['Ascender'] = $info['FontBBox'][3]; + if(!isset($info['Descender'])) + $info['Descender'] = $info['FontBBox'][1]; + $info['Bold'] = isset($info['Weight']) && preg_match('/bold|black/i', $info['Weight']); + if(isset($cw['.notdef'])) + $info['MissingWidth'] = $cw['.notdef']; + else + $info['MissingWidth'] = 0; + $widths = array_fill(0, 256, $info['MissingWidth']); + foreach($map as $c=>$v) + { + if($v['name']!='.notdef') + { + if(isset($cw[$v['name']])) + $widths[$c] = $cw[$v['name']]; + else + Warning('Character '.$v['name'].' is missing'); + } + } + $info['Widths'] = $widths; + return $info; +} + +function MakeFontDescriptor($info) +{ + // Ascent + $fd = "array('Ascent'=>".$info['Ascender']; + // Descent + $fd .= ",'Descent'=>".$info['Descender']; + // CapHeight + if(!empty($info['CapHeight'])) + $fd .= ",'CapHeight'=>".$info['CapHeight']; + else + $fd .= ",'CapHeight'=>".$info['Ascender']; + // Flags + $flags = 0; + if($info['IsFixedPitch']) + $flags += 1<<0; + $flags += 1<<5; + if($info['ItalicAngle']!=0) + $flags += 1<<6; + $fd .= ",'Flags'=>".$flags; + // FontBBox + $fbb = $info['FontBBox']; + $fd .= ",'FontBBox'=>'[".$fbb[0].' '.$fbb[1].' '.$fbb[2].' '.$fbb[3]."]'"; + // ItalicAngle + $fd .= ",'ItalicAngle'=>".$info['ItalicAngle']; + // StemV + if(isset($info['StdVW'])) + $stemv = $info['StdVW']; + elseif($info['Bold']) + $stemv = 120; + else + $stemv = 70; + $fd .= ",'StemV'=>".$stemv; + // MissingWidth + $fd .= ",'MissingWidth'=>".$info['MissingWidth'].')'; + return $fd; +} + +function MakeWidthArray($widths) +{ + $s = "array(\n\t"; + for($c=0;$c<=255;$c++) + { + if(chr($c)=="'") + $s .= "'\\''"; + elseif(chr($c)=="\\") + $s .= "'\\\\'"; + elseif($c>=32 && $c<=126) + $s .= "'".chr($c)."'"; + else + $s .= "chr($c)"; + $s .= '=>'.$widths[$c]; + if($c<255) + $s .= ','; + if(($c+1)%22==0) + $s .= "\n\t"; + } + $s .= ')'; + return $s; +} + +function MakeFontEncoding($map) +{ + // Build differences from reference encoding + $ref = LoadMap('cp1252'); + $s = ''; + $last = 0; + for($c=32;$c<=255;$c++) + { + if($map[$c]['name']!=$ref[$c]['name']) + { + if($c!=$last+1) + $s .= $c.' '; + $last = $c; + $s .= '/'.$map[$c]['name'].' '; + } + } + return rtrim($s); +} + +function MakeUnicodeArray($map) +{ + // Build mapping to Unicode values + $ranges = array(); + foreach($map as $c=>$v) + { + $uv = $v['uv']; + if($uv!=-1) + { + if(isset($range)) + { + if($c==$range[1]+1 && $uv==$range[3]+1) + { + $range[1]++; + $range[3]++; + } + else + { + $ranges[] = $range; + $range = array($c, $c, $uv, $uv); + } + } + else + $range = array($c, $c, $uv, $uv); + } + } + $ranges[] = $range; + + foreach($ranges as $range) + { + if(isset($s)) + $s .= ','; + else + $s = 'array('; + $s .= $range[0].'=>'; + $nb = $range[1]-$range[0]+1; + if($nb>1) + $s .= 'array('.$range[2].','.$nb.')'; + else + $s .= $range[2]; + } + $s .= ')'; + return $s; +} + +function SaveToFile($file, $s, $mode) +{ + $f = fopen($file, 'w'.$mode); + if(!$f) + Error('Can\'t write to file '.$file); + fwrite($f, $s); + fclose($f); +} + +function MakeDefinitionFile($file, $type, $enc, $embed, $subset, $map, $info) +{ + $s = "\n"; + SaveToFile($file, $s, 't'); +} + +function MakeFont($fontfile, $enc='cp1252', $embed=true, $subset=true) +{ + // Generate a font definition file + if(!file_exists($fontfile)) + Error('Font file not found: '.$fontfile); + $ext = strtolower(substr($fontfile,-3)); + if($ext=='ttf' || $ext=='otf') + $type = 'TrueType'; + elseif($ext=='pfb') + $type = 'Type1'; + else + Error('Unrecognized font file extension: '.$ext); + + $map = LoadMap($enc); + + if($type=='TrueType') + $info = GetInfoFromTrueType($fontfile, $embed, $subset, $map); + else + $info = GetInfoFromType1($fontfile, $embed, $map); + + $basename = substr(basename($fontfile), 0, -4); + if($embed) + { + if(function_exists('gzcompress')) + { + $file = $basename.'.z'; + SaveToFile($file, gzcompress($info['Data']), 'b'); + $info['File'] = $file; + Message('Font file compressed: '.$file); + } + else + { + $info['File'] = basename($fontfile); + $subset = false; + Notice('Font file could not be compressed (zlib extension not available)'); + } + } + + MakeDefinitionFile($basename.'.php', $type, $enc, $embed, $subset, $map, $info); + Message('Font definition file generated: '.$basename.'.php'); +} + +if(PHP_SAPI=='cli') +{ + // Command-line interface + ini_set('log_errors', '0'); + if($argc==1) + die("Usage: php makefont.php fontfile [encoding] [embed] [subset]\n"); + $fontfile = $argv[1]; + if($argc>=3) + $enc = $argv[2]; + else + $enc = 'cp1252'; + if($argc>=4) + $embed = ($argv[3]=='true' || $argv[3]=='1'); + else + $embed = true; + if($argc>=5) + $subset = ($argv[4]=='true' || $argv[4]=='1'); + else + $subset = true; + MakeFont($fontfile, $enc, $embed, $subset); +} +?> diff --git a/application/third_party/fpdf/makefont/ttfparser.php b/application/third_party/fpdf/makefont/ttfparser.php new file mode 100644 index 0000000..b5acf29 --- /dev/null +++ b/application/third_party/fpdf/makefont/ttfparser.php @@ -0,0 +1,714 @@ +f = fopen($file, 'rb'); + if(!$this->f) + $this->Error('Can\'t open file: '.$file); + } + + function __destruct() + { + if(is_resource($this->f)) + fclose($this->f); + } + + function Parse() + { + $this->ParseOffsetTable(); + $this->ParseHead(); + $this->ParseHhea(); + $this->ParseMaxp(); + $this->ParseHmtx(); + $this->ParseLoca(); + $this->ParseGlyf(); + $this->ParseCmap(); + $this->ParseName(); + $this->ParseOS2(); + $this->ParsePost(); + } + + function ParseOffsetTable() + { + $version = $this->Read(4); + if($version=='OTTO') + $this->Error('OpenType fonts based on PostScript outlines are not supported'); + if($version!="\x00\x01\x00\x00") + $this->Error('Unrecognized file format'); + $numTables = $this->ReadUShort(); + $this->Skip(3*2); // searchRange, entrySelector, rangeShift + $this->tables = array(); + for($i=0;$i<$numTables;$i++) + { + $tag = $this->Read(4); + $checkSum = $this->Read(4); + $offset = $this->ReadULong(); + $length = $this->ReadULong(); + $this->tables[$tag] = array('offset'=>$offset, 'length'=>$length, 'checkSum'=>$checkSum); + } + } + + function ParseHead() + { + $this->Seek('head'); + $this->Skip(3*4); // version, fontRevision, checkSumAdjustment + $magicNumber = $this->ReadULong(); + if($magicNumber!=0x5F0F3CF5) + $this->Error('Incorrect magic number'); + $this->Skip(2); // flags + $this->unitsPerEm = $this->ReadUShort(); + $this->Skip(2*8); // created, modified + $this->xMin = $this->ReadShort(); + $this->yMin = $this->ReadShort(); + $this->xMax = $this->ReadShort(); + $this->yMax = $this->ReadShort(); + $this->Skip(3*2); // macStyle, lowestRecPPEM, fontDirectionHint + $this->indexToLocFormat = $this->ReadShort(); + } + + function ParseHhea() + { + $this->Seek('hhea'); + $this->Skip(4+15*2); + $this->numberOfHMetrics = $this->ReadUShort(); + } + + function ParseMaxp() + { + $this->Seek('maxp'); + $this->Skip(4); + $this->numGlyphs = $this->ReadUShort(); + } + + function ParseHmtx() + { + $this->Seek('hmtx'); + $this->glyphs = array(); + for($i=0;$i<$this->numberOfHMetrics;$i++) + { + $advanceWidth = $this->ReadUShort(); + $lsb = $this->ReadShort(); + $this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb); + } + for($i=$this->numberOfHMetrics;$i<$this->numGlyphs;$i++) + { + $lsb = $this->ReadShort(); + $this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb); + } + } + + function ParseLoca() + { + $this->Seek('loca'); + $offsets = array(); + if($this->indexToLocFormat==0) + { + // Short format + for($i=0;$i<=$this->numGlyphs;$i++) + $offsets[] = 2*$this->ReadUShort(); + } + else + { + // Long format + for($i=0;$i<=$this->numGlyphs;$i++) + $offsets[] = $this->ReadULong(); + } + for($i=0;$i<$this->numGlyphs;$i++) + { + $this->glyphs[$i]['offset'] = $offsets[$i]; + $this->glyphs[$i]['length'] = $offsets[$i+1] - $offsets[$i]; + } + } + + function ParseGlyf() + { + $tableOffset = $this->tables['glyf']['offset']; + foreach($this->glyphs as &$glyph) + { + if($glyph['length']>0) + { + fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET); + if($this->ReadShort()<0) + { + // Composite glyph + $this->Skip(4*2); // xMin, yMin, xMax, yMax + $offset = 5*2; + $a = array(); + do + { + $flags = $this->ReadUShort(); + $index = $this->ReadUShort(); + $a[$offset+2] = $index; + if($flags & 1) // ARG_1_AND_2_ARE_WORDS + $skip = 2*2; + else + $skip = 2; + if($flags & 8) // WE_HAVE_A_SCALE + $skip += 2; + elseif($flags & 64) // WE_HAVE_AN_X_AND_Y_SCALE + $skip += 2*2; + elseif($flags & 128) // WE_HAVE_A_TWO_BY_TWO + $skip += 4*2; + $this->Skip($skip); + $offset += 2*2 + $skip; + } + while($flags & 32); // MORE_COMPONENTS + $glyph['components'] = $a; + } + } + } + } + + function ParseCmap() + { + $this->Seek('cmap'); + $this->Skip(2); // version + $numTables = $this->ReadUShort(); + $offset31 = 0; + for($i=0;$i<$numTables;$i++) + { + $platformID = $this->ReadUShort(); + $encodingID = $this->ReadUShort(); + $offset = $this->ReadULong(); + if($platformID==3 && $encodingID==1) + $offset31 = $offset; + } + if($offset31==0) + $this->Error('No Unicode encoding found'); + + $startCount = array(); + $endCount = array(); + $idDelta = array(); + $idRangeOffset = array(); + $this->chars = array(); + fseek($this->f, $this->tables['cmap']['offset']+$offset31, SEEK_SET); + $format = $this->ReadUShort(); + if($format!=4) + $this->Error('Unexpected subtable format: '.$format); + $this->Skip(2*2); // length, language + $segCount = $this->ReadUShort()/2; + $this->Skip(3*2); // searchRange, entrySelector, rangeShift + for($i=0;$i<$segCount;$i++) + $endCount[$i] = $this->ReadUShort(); + $this->Skip(2); // reservedPad + for($i=0;$i<$segCount;$i++) + $startCount[$i] = $this->ReadUShort(); + for($i=0;$i<$segCount;$i++) + $idDelta[$i] = $this->ReadShort(); + $offset = ftell($this->f); + for($i=0;$i<$segCount;$i++) + $idRangeOffset[$i] = $this->ReadUShort(); + + for($i=0;$i<$segCount;$i++) + { + $c1 = $startCount[$i]; + $c2 = $endCount[$i]; + $d = $idDelta[$i]; + $ro = $idRangeOffset[$i]; + if($ro>0) + fseek($this->f, $offset+2*$i+$ro, SEEK_SET); + for($c=$c1;$c<=$c2;$c++) + { + if($c==0xFFFF) + break; + if($ro>0) + { + $gid = $this->ReadUShort(); + if($gid>0) + $gid += $d; + } + else + $gid = $c+$d; + if($gid>=65536) + $gid -= 65536; + if($gid>0) + $this->chars[$c] = $gid; + } + } + } + + function ParseName() + { + $this->Seek('name'); + $tableOffset = $this->tables['name']['offset']; + $this->postScriptName = ''; + $this->Skip(2); // format + $count = $this->ReadUShort(); + $stringOffset = $this->ReadUShort(); + for($i=0;$i<$count;$i++) + { + $this->Skip(3*2); // platformID, encodingID, languageID + $nameID = $this->ReadUShort(); + $length = $this->ReadUShort(); + $offset = $this->ReadUShort(); + if($nameID==6) + { + // PostScript name + fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET); + $s = $this->Read($length); + $s = str_replace(chr(0), '', $s); + $s = preg_replace('|[ \[\](){}<>/%]|', '', $s); + $this->postScriptName = $s; + break; + } + } + if($this->postScriptName=='') + $this->Error('PostScript name not found'); + } + + function ParseOS2() + { + $this->Seek('OS/2'); + $version = $this->ReadUShort(); + $this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass + $fsType = $this->ReadUShort(); + $this->embeddable = ($fsType!=2) && ($fsType & 0x200)==0; + $this->Skip(11*2+10+4*4+4); + $fsSelection = $this->ReadUShort(); + $this->bold = ($fsSelection & 32)!=0; + $this->Skip(2*2); // usFirstCharIndex, usLastCharIndex + $this->typoAscender = $this->ReadShort(); + $this->typoDescender = $this->ReadShort(); + if($version>=2) + { + $this->Skip(3*2+2*4+2); + $this->capHeight = $this->ReadShort(); + } + else + $this->capHeight = 0; + } + + function ParsePost() + { + $this->Seek('post'); + $version = $this->ReadULong(); + $this->italicAngle = $this->ReadShort(); + $this->Skip(2); // Skip decimal part + $this->underlinePosition = $this->ReadShort(); + $this->underlineThickness = $this->ReadShort(); + $this->isFixedPitch = ($this->ReadULong()!=0); + if($version==0x20000) + { + // Extract glyph names + $this->Skip(4*4); // min/max usage + $this->Skip(2); // numberOfGlyphs + $glyphNameIndex = array(); + $names = array(); + $numNames = 0; + for($i=0;$i<$this->numGlyphs;$i++) + { + $index = $this->ReadUShort(); + $glyphNameIndex[] = $index; + if($index>=258 && $index-257>$numNames) + $numNames = $index-257; + } + for($i=0;$i<$numNames;$i++) + { + $len = ord($this->Read(1)); + $names[] = $this->Read($len); + } + foreach($glyphNameIndex as $i=>$index) + { + if($index>=258) + $this->glyphs[$i]['name'] = $names[$index-258]; + else + $this->glyphs[$i]['name'] = $index; + } + $this->glyphNames = true; + } + else + $this->glyphNames = false; + } + + function Subset($chars) + { + $this->subsettedGlyphs = array(); + $this->AddGlyph(0); + $this->subsettedChars = array(); + foreach($chars as $char) + { + if(isset($this->chars[$char])) + { + $this->subsettedChars[] = $char; + $this->AddGlyph($this->chars[$char]); + } + } + } + + function AddGlyph($id) + { + if(!isset($this->glyphs[$id]['ssid'])) + { + $this->glyphs[$id]['ssid'] = count($this->subsettedGlyphs); + $this->subsettedGlyphs[] = $id; + if(isset($this->glyphs[$id]['components'])) + { + foreach($this->glyphs[$id]['components'] as $cid) + $this->AddGlyph($cid); + } + } + } + + function Build() + { + $this->BuildCmap(); + $this->BuildHhea(); + $this->BuildHmtx(); + $this->BuildLoca(); + $this->BuildGlyf(); + $this->BuildMaxp(); + $this->BuildPost(); + return $this->BuildFont(); + } + + function BuildCmap() + { + if(!isset($this->subsettedChars)) + return; + + // Divide charset in contiguous segments + $chars = $this->subsettedChars; + sort($chars); + $segments = array(); + $segment = array($chars[0], $chars[0]); + for($i=1;$i$segment[1]+1) + { + $segments[] = $segment; + $segment = array($chars[$i], $chars[$i]); + } + else + $segment[1]++; + } + $segments[] = $segment; + $segments[] = array(0xFFFF, 0xFFFF); + $segCount = count($segments); + + // Build a Format 4 subtable + $startCount = array(); + $endCount = array(); + $idDelta = array(); + $idRangeOffset = array(); + $glyphIdArray = ''; + for($i=0;$i<$segCount;$i++) + { + list($start, $end) = $segments[$i]; + $startCount[] = $start; + $endCount[] = $end; + if($start!=$end) + { + // Segment with multiple chars + $idDelta[] = 0; + $idRangeOffset[] = strlen($glyphIdArray) + ($segCount-$i)*2; + for($c=$start;$c<=$end;$c++) + { + $ssid = $this->glyphs[$this->chars[$c]]['ssid']; + $glyphIdArray .= pack('n', $ssid); + } + } + else + { + // Segment with a single char + if($start<0xFFFF) + $ssid = $this->glyphs[$this->chars[$start]]['ssid']; + else + $ssid = 0; + $idDelta[] = $ssid - $start; + $idRangeOffset[] = 0; + } + } + $entrySelector = 0; + $n = $segCount; + while($n!=1) + { + $n = $n>>1; + $entrySelector++; + } + $searchRange = (1<<$entrySelector)*2; + $rangeShift = 2*$segCount - $searchRange; + $cmap = pack('nnnn', 2*$segCount, $searchRange, $entrySelector, $rangeShift); + foreach($endCount as $val) + $cmap .= pack('n', $val); + $cmap .= pack('n', 0); // reservedPad + foreach($startCount as $val) + $cmap .= pack('n', $val); + foreach($idDelta as $val) + $cmap .= pack('n', $val); + foreach($idRangeOffset as $val) + $cmap .= pack('n', $val); + $cmap .= $glyphIdArray; + + $data = pack('nn', 0, 1); // version, numTables + $data .= pack('nnN', 3, 1, 12); // platformID, encodingID, offset + $data .= pack('nnn', 4, 6+strlen($cmap), 0); // format, length, language + $data .= $cmap; + $this->SetTable('cmap', $data); + } + + function BuildHhea() + { + $this->LoadTable('hhea'); + $numberOfHMetrics = count($this->subsettedGlyphs); + $data = substr_replace($this->tables['hhea']['data'], pack('n',$numberOfHMetrics), 4+15*2, 2); + $this->SetTable('hhea', $data); + } + + function BuildHmtx() + { + $data = ''; + foreach($this->subsettedGlyphs as $id) + { + $glyph = $this->glyphs[$id]; + $data .= pack('nn', $glyph['w'], $glyph['lsb']); + } + $this->SetTable('hmtx', $data); + } + + function BuildLoca() + { + $data = ''; + $offset = 0; + foreach($this->subsettedGlyphs as $id) + { + if($this->indexToLocFormat==0) + $data .= pack('n', $offset/2); + else + $data .= pack('N', $offset); + $offset += $this->glyphs[$id]['length']; + } + if($this->indexToLocFormat==0) + $data .= pack('n', $offset/2); + else + $data .= pack('N', $offset); + $this->SetTable('loca', $data); + } + + function BuildGlyf() + { + $tableOffset = $this->tables['glyf']['offset']; + $data = ''; + foreach($this->subsettedGlyphs as $id) + { + $glyph = $this->glyphs[$id]; + fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET); + $glyph_data = $this->Read($glyph['length']); + if(isset($glyph['components'])) + { + // Composite glyph + foreach($glyph['components'] as $offset=>$cid) + { + $ssid = $this->glyphs[$cid]['ssid']; + $glyph_data = substr_replace($glyph_data, pack('n',$ssid), $offset, 2); + } + } + $data .= $glyph_data; + } + $this->SetTable('glyf', $data); + } + + function BuildMaxp() + { + $this->LoadTable('maxp'); + $numGlyphs = count($this->subsettedGlyphs); + $data = substr_replace($this->tables['maxp']['data'], pack('n',$numGlyphs), 4, 2); + $this->SetTable('maxp', $data); + } + + function BuildPost() + { + $this->Seek('post'); + if($this->glyphNames) + { + // Version 2.0 + $numberOfGlyphs = count($this->subsettedGlyphs); + $numNames = 0; + $names = ''; + $data = $this->Read(2*4+2*2+5*4); + $data .= pack('n', $numberOfGlyphs); + foreach($this->subsettedGlyphs as $id) + { + $name = $this->glyphs[$id]['name']; + if(is_string($name)) + { + $data .= pack('n', 258+$numNames); + $names .= chr(strlen($name)).$name; + $numNames++; + } + else + $data .= pack('n', $name); + } + $data .= $names; + } + else + { + // Version 3.0 + $this->Skip(4); + $data = "\x00\x03\x00\x00"; + $data .= $this->Read(4+2*2+5*4); + } + $this->SetTable('post', $data); + } + + function BuildFont() + { + $tags = array(); + foreach(array('cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep') as $tag) + { + if(isset($this->tables[$tag])) + $tags[] = $tag; + } + $numTables = count($tags); + $offset = 12 + 16*$numTables; + foreach($tags as $tag) + { + if(!isset($this->tables[$tag]['data'])) + $this->LoadTable($tag); + $this->tables[$tag]['offset'] = $offset; + $offset += strlen($this->tables[$tag]['data']); + } + + // Build offset table + $entrySelector = 0; + $n = $numTables; + while($n!=1) + { + $n = $n>>1; + $entrySelector++; + } + $searchRange = 16*(1<<$entrySelector); + $rangeShift = 16*$numTables - $searchRange; + $offsetTable = pack('nnnnnn', 1, 0, $numTables, $searchRange, $entrySelector, $rangeShift); + foreach($tags as $tag) + { + $table = $this->tables[$tag]; + $offsetTable .= $tag.$table['checkSum'].pack('NN', $table['offset'], $table['length']); + } + + // Compute checkSumAdjustment (0xB1B0AFBA - font checkSum) + $s = $this->CheckSum($offsetTable); + foreach($tags as $tag) + $s .= $this->tables[$tag]['checkSum']; + $a = unpack('n2', $this->CheckSum($s)); + $high = 0xB1B0 + ($a[1]^0xFFFF); + $low = 0xAFBA + ($a[2]^0xFFFF) + 1; + $checkSumAdjustment = pack('nn', $high+($low>>16), $low); + $this->tables['head']['data'] = substr_replace($this->tables['head']['data'], $checkSumAdjustment, 8, 4); + + $font = $offsetTable; + foreach($tags as $tag) + $font .= $this->tables[$tag]['data']; + + return $font; + } + + function LoadTable($tag) + { + $this->Seek($tag); + $length = $this->tables[$tag]['length']; + $n = $length % 4; + if($n>0) + $length += 4 - $n; + $this->tables[$tag]['data'] = $this->Read($length); + } + + function SetTable($tag, $data) + { + $length = strlen($data); + $n = $length % 4; + if($n>0) + $data = str_pad($data, $length+4-$n, "\x00"); + $this->tables[$tag]['data'] = $data; + $this->tables[$tag]['length'] = $length; + $this->tables[$tag]['checkSum'] = $this->CheckSum($data); + } + + function Seek($tag) + { + if(!isset($this->tables[$tag])) + $this->Error('Table not found: '.$tag); + fseek($this->f, $this->tables[$tag]['offset'], SEEK_SET); + } + + function Skip($n) + { + fseek($this->f, $n, SEEK_CUR); + } + + function Read($n) + { + return $n>0 ? fread($this->f, $n) : ''; + } + + function ReadUShort() + { + $a = unpack('nn', fread($this->f,2)); + return $a['n']; + } + + function ReadShort() + { + $a = unpack('nn', fread($this->f,2)); + $v = $a['n']; + if($v>=0x8000) + $v -= 65536; + return $v; + } + + function ReadULong() + { + $a = unpack('NN', fread($this->f,4)); + return $a['N']; + } + + function CheckSum($s) + { + $n = strlen($s); + $high = 0; + $low = 0; + for($i=0;$i<$n;$i+=4) + { + $high += (ord($s[$i])<<8) + ord($s[$i+1]); + $low += (ord($s[$i+2])<<8) + ord($s[$i+3]); + } + return pack('nn', $high+($low>>16), $low); + } + + function Error($msg) + { + throw new Exception($msg); + } +} +?> diff --git a/application/third_party/fpdf/tutorial/20k_c1.txt b/application/third_party/fpdf/tutorial/20k_c1.txt new file mode 100644 index 0000000..0b09f26 --- /dev/null +++ b/application/third_party/fpdf/tutorial/20k_c1.txt @@ -0,0 +1,10 @@ +The year 1866 was marked by a bizarre development, an unexplained and downright inexplicable phenomenon that surely no one has forgotten. Without getting into those rumors that upset civilians in the seaports and deranged the public mind even far inland, it must be said that professional seamen were especially alarmed. Traders, shipowners, captains of vessels, skippers, and master mariners from Europe and America, naval officers from every country, and at their heels the various national governments on these two continents, were all extremely disturbed by the business. +In essence, over a period of time several ships had encountered "an enormous thing" at sea, a long spindle-shaped object, sometimes giving off a phosphorescent glow, infinitely bigger and faster than any whale. +The relevant data on this apparition, as recorded in various logbooks, agreed pretty closely as to the structure of the object or creature in question, its unprecedented speed of movement, its startling locomotive power, and the unique vitality with which it seemed to be gifted. If it was a cetacean, it exceeded in bulk any whale previously classified by science. No naturalist, neither Cuvier nor Lacépède, neither Professor Dumeril nor Professor de Quatrefages, would have accepted the existence of such a monster sight unseen -- specifically, unseen by their own scientific eyes. +Striking an average of observations taken at different times -- rejecting those timid estimates that gave the object a length of 200 feet, and ignoring those exaggerated views that saw it as a mile wide and three long--you could still assert that this phenomenal creature greatly exceeded the dimensions of anything then known to ichthyologists, if it existed at all. +Now then, it did exist, this was an undeniable fact; and since the human mind dotes on objects of wonder, you can understand the worldwide excitement caused by this unearthly apparition. As for relegating it to the realm of fiction, that charge had to be dropped. +In essence, on July 20, 1866, the steamer Governor Higginson, from the Calcutta & Burnach Steam Navigation Co., encountered this moving mass five miles off the eastern shores of Australia. Captain Baker at first thought he was in the presence of an unknown reef; he was even about to fix its exact position when two waterspouts shot out of this inexplicable object and sprang hissing into the air some 150 feet. So, unless this reef was subject to the intermittent eruptions of a geyser, the Governor Higginson had fair and honest dealings with some aquatic mammal, until then unknown, that could spurt from its blowholes waterspouts mixed with air and steam. +Similar events were likewise observed in Pacific seas, on July 23 of the same year, by the Christopher Columbus from the West India & Pacific Steam Navigation Co. Consequently, this extraordinary cetacean could transfer itself from one locality to another with startling swiftness, since within an interval of just three days, the Governor Higginson and the Christopher Columbus had observed it at two positions on the charts separated by a distance of more than 700 nautical leagues. +Fifteen days later and 2,000 leagues farther, the Helvetia from the Compagnie Nationale and the Shannon from the Royal Mail line, running on opposite tacks in that part of the Atlantic lying between the United States and Europe, respectively signaled each other that the monster had been sighted in latitude 42 degrees 15' north and longitude 60 degrees 35' west of the meridian of Greenwich. From their simultaneous observations, they were able to estimate the mammal's minimum length at more than 350 English feet; this was because both the Shannon and the Helvetia were of smaller dimensions, although each measured 100 meters stem to stern. Now then, the biggest whales, those rorqual whales that frequent the waterways of the Aleutian Islands, have never exceeded a length of 56 meters--if they reach even that. +One after another, reports arrived that would profoundly affect public opinion: new observations taken by the transatlantic liner Pereire, the Inman line's Etna running afoul of the monster, an official report drawn up by officers on the French frigate Normandy, dead-earnest reckonings obtained by the general staff of Commodore Fitz-James aboard the Lord Clyde. In lighthearted countries, people joked about this phenomenon, but such serious, practical countries as England, America, and Germany were deeply concerned. +In every big city the monster was the latest rage; they sang about it in the coffee houses, they ridiculed it in the newspapers, they dramatized it in the theaters. The tabloids found it a fine opportunity for hatching all sorts of hoaxes. In those newspapers short of copy, you saw the reappearance of every gigantic imaginary creature, from "Moby Dick," that dreadful white whale from the High Arctic regions, to the stupendous kraken whose tentacles could entwine a 500-ton craft and drag it into the ocean depths. They even reprinted reports from ancient times: the views of Aristotle and Pliny accepting the existence of such monsters, then the Norwegian stories of Bishop Pontoppidan, the narratives of Paul Egede, and finally the reports of Captain Harrington -- whose good faith is above suspicion--in which he claims he saw, while aboard the Castilian in 1857, one of those enormous serpents that, until then, had frequented only the seas of France's old extremist newspaper, The Constitutionalist. diff --git a/application/third_party/fpdf/tutorial/20k_c2.txt b/application/third_party/fpdf/tutorial/20k_c2.txt new file mode 100644 index 0000000..096dbd1 --- /dev/null +++ b/application/third_party/fpdf/tutorial/20k_c2.txt @@ -0,0 +1,23 @@ +During the period in which these developments were occurring, I had returned from a scientific undertaking organized to explore the Nebraska badlands in the United States. In my capacity as Assistant Professor at the Paris Museum of Natural History, I had been attached to this expedition by the French government. After spending six months in Nebraska, I arrived in New York laden with valuable collections near the end of March. My departure for France was set for early May. In the meantime, then, I was busy classifying my mineralogical, botanical, and zoological treasures when that incident took place with the Scotia. +I was perfectly abreast of this question, which was the big news of the day, and how could I not have been? I had read and reread every American and European newspaper without being any farther along. This mystery puzzled me. Finding it impossible to form any views, I drifted from one extreme to the other. Something was out there, that much was certain, and any doubting Thomas was invited to place his finger on the Scotia's wound. +When I arrived in New York, the question was at the boiling point. The hypothesis of a drifting islet or an elusive reef, put forward by people not quite in their right minds, was completely eliminated. And indeed, unless this reef had an engine in its belly, how could it move about with such prodigious speed? +Also discredited was the idea of a floating hull or some other enormous wreckage, and again because of this speed of movement. +So only two possible solutions to the question were left, creating two very distinct groups of supporters: on one side, those favoring a monster of colossal strength; on the other, those favoring an "underwater boat" of tremendous motor power. +Now then, although the latter hypothesis was completely admissible, it couldn't stand up to inquiries conducted in both the New World and the Old. That a private individual had such a mechanism at his disposal was less than probable. Where and when had he built it, and how could he have built it in secret? +Only some government could own such an engine of destruction, and in these disaster-filled times, when men tax their ingenuity to build increasingly powerful aggressive weapons, it was possible that, unknown to the rest of the world, some nation could have been testing such a fearsome machine. The Chassepot rifle led to the torpedo, and the torpedo has led to this underwater battering ram, which in turn will lead to the world putting its foot down. At least I hope it will. +But this hypothesis of a war machine collapsed in the face of formal denials from the various governments. Since the public interest was at stake and transoceanic travel was suffering, the sincerity of these governments could not be doubted. Besides, how could the assembly of this underwater boat have escaped public notice? Keeping a secret under such circumstances would be difficult enough for an individual, and certainly impossible for a nation whose every move is under constant surveillance by rival powers. +So, after inquiries conducted in England, France, Russia, Prussia, Spain, Italy, America, and even Turkey, the hypothesis of an underwater Monitor was ultimately rejected. +After I arrived in New York, several people did me the honor of consulting me on the phenomenon in question. In France I had published a two-volume work, in quarto, entitled The Mysteries of the Great Ocean Depths. Well received in scholarly circles, this book had established me as a specialist in this pretty obscure field of natural history. My views were in demand. As long as I could deny the reality of the business, I confined myself to a flat "no comment." But soon, pinned to the wall, I had to explain myself straight out. And in this vein, "the honorable Pierre Aronnax, Professor at the Paris Museum," was summoned by The New York Herald to formulate his views no matter what. +I complied. Since I could no longer hold my tongue, I let it wag. I discussed the question in its every aspect, both political and scientific, and this is an excerpt from the well-padded article I published in the issue of April 30. + +"Therefore," I wrote, "after examining these different hypotheses one by one, we are forced, every other supposition having been refuted, to accept the existence of an extremely powerful marine animal. +"The deepest parts of the ocean are totally unknown to us. No soundings have been able to reach them. What goes on in those distant depths? What creatures inhabit, or could inhabit, those regions twelve or fifteen miles beneath the surface of the water? What is the constitution of these animals? It's almost beyond conjecture. +"However, the solution to this problem submitted to me can take the form of a choice between two alternatives. +"Either we know every variety of creature populating our planet, or we do not. +"If we do not know every one of them, if nature still keeps ichthyological secrets from us, nothing is more admissible than to accept the existence of fish or cetaceans of new species or even new genera, animals with a basically 'cast-iron' constitution that inhabit strata beyond the reach of our soundings, and which some development or other, an urge or a whim if you prefer, can bring to the upper level of the ocean for long intervals. +"If, on the other hand, we do know every living species, we must look for the animal in question among those marine creatures already cataloged, and in this event I would be inclined to accept the existence of a giant narwhale. +"The common narwhale, or sea unicorn, often reaches a length of sixty feet. Increase its dimensions fivefold or even tenfold, then give this cetacean a strength in proportion to its size while enlarging its offensive weapons, and you have the animal we're looking for. It would have the proportions determined by the officers of the Shannon, the instrument needed to perforate the Scotia, and the power to pierce a steamer's hull. +"In essence, the narwhale is armed with a sort of ivory sword, or lance, as certain naturalists have expressed it. It's a king-sized tooth as hard as steel. Some of these teeth have been found buried in the bodies of baleen whales, which the narwhale attacks with invariable success. Others have been wrenched, not without difficulty, from the undersides of vessels that narwhales have pierced clean through, as a gimlet pierces a wine barrel. The museum at the Faculty of Medicine in Paris owns one of these tusks with a length of 2.25 meters and a width at its base of forty-eight centimeters! +"All right then! Imagine this weapon to be ten times stronger and the animal ten times more powerful, launch it at a speed of twenty miles per hour, multiply its mass times its velocity, and you get just the collision we need to cause the specified catastrophe. +"So, until information becomes more abundant, I plump for a sea unicorn of colossal dimensions, no longer armed with a mere lance but with an actual spur, like ironclad frigates or those warships called 'rams,' whose mass and motor power it would possess simultaneously. +"This inexplicable phenomenon is thus explained away--unless it's something else entirely, which, despite everything that has been sighted, studied, explored and experienced, is still possible!" diff --git a/application/third_party/fpdf/tutorial/CevicheOne-Regular-Licence.txt b/application/third_party/fpdf/tutorial/CevicheOne-Regular-Licence.txt new file mode 100644 index 0000000..e7a39c9 --- /dev/null +++ b/application/third_party/fpdf/tutorial/CevicheOne-Regular-Licence.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011 by LatinoType Limitada (luciano@latinotype.com), +with Reserved Font Names "Cecivhe" and "Ceviche One" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/application/third_party/fpdf/tutorial/CevicheOne-Regular.php b/application/third_party/fpdf/tutorial/CevicheOne-Regular.php new file mode 100644 index 0000000..c2f8f60 --- /dev/null +++ b/application/third_party/fpdf/tutorial/CevicheOne-Regular.php @@ -0,0 +1,25 @@ +806,'Descent'=>-237,'CapHeight'=>425,'Flags'=>32,'FontBBox'=>'[-42 -237 1427 806]','ItalicAngle'=>0,'StemV'=>70,'MissingWidth'=>294); +$up = -75; +$ut = 50; +$cw = array( + chr(0)=>294,chr(1)=>294,chr(2)=>294,chr(3)=>294,chr(4)=>294,chr(5)=>294,chr(6)=>294,chr(7)=>294,chr(8)=>294,chr(9)=>294,chr(10)=>294,chr(11)=>294,chr(12)=>294,chr(13)=>294,chr(14)=>294,chr(15)=>294,chr(16)=>294,chr(17)=>294,chr(18)=>294,chr(19)=>294,chr(20)=>294,chr(21)=>294, + chr(22)=>294,chr(23)=>294,chr(24)=>294,chr(25)=>294,chr(26)=>294,chr(27)=>294,chr(28)=>294,chr(29)=>294,chr(30)=>294,chr(31)=>294,' '=>130,'!'=>254,'"'=>334,'#'=>496,'$'=>469,'%'=>765,'&'=>761,'\''=>148,'('=>268,')'=>269,'*'=>435,'+'=>442, + ','=>235,'-'=>339,'.'=>233,'/'=>301,'0'=>512,'1'=>252,'2'=>501,'3'=>471,'4'=>537,'5'=>471,'6'=>501,'7'=>424,'8'=>506,'9'=>502,':'=>292,';'=>299,'<'=>463,'='=>483,'>'=>459,'?'=>453,'@'=>672,'A'=>511, + 'B'=>573,'C'=>480,'D'=>541,'E'=>507,'F'=>490,'G'=>515,'H'=>517,'I'=>250,'J'=>270,'K'=>570,'L'=>368,'M'=>620,'N'=>548,'O'=>507,'P'=>540,'Q'=>532,'R'=>552,'S'=>490,'T'=>434,'U'=>527,'V'=>514,'W'=>670, + 'X'=>541,'Y'=>497,'Z'=>499,'['=>288,'\\'=>472,']'=>288,'^'=>448,'_'=>350,'`'=>449,'a'=>446,'b'=>454,'c'=>394,'d'=>462,'e'=>404,'f'=>332,'g'=>458,'h'=>446,'i'=>234,'j'=>232,'k'=>457,'l'=>231,'m'=>659, + 'n'=>450,'o'=>412,'p'=>436,'q'=>468,'r'=>389,'s'=>394,'t'=>318,'u'=>458,'v'=>421,'w'=>612,'x'=>450,'y'=>429,'z'=>423,'{'=>312,'|'=>231,'}'=>312,'~'=>504,chr(127)=>294,chr(128)=>586,chr(129)=>294,chr(130)=>176,chr(131)=>377, + chr(132)=>361,chr(133)=>761,chr(134)=>422,chr(135)=>425,chr(136)=>448,chr(137)=>1050,chr(138)=>490,chr(139)=>377,chr(140)=>787,chr(141)=>294,chr(142)=>499,chr(143)=>294,chr(144)=>294,chr(145)=>171,chr(146)=>148,chr(147)=>357,chr(148)=>334,chr(149)=>266,chr(150)=>444,chr(151)=>554,chr(152)=>416,chr(153)=>608, + chr(154)=>394,chr(155)=>378,chr(156)=>674,chr(157)=>294,chr(158)=>423,chr(159)=>497,chr(160)=>130,chr(161)=>253,chr(162)=>424,chr(163)=>510,chr(164)=>665,chr(165)=>551,chr(166)=>252,chr(167)=>413,chr(168)=>505,chr(169)=>675,chr(170)=>361,chr(171)=>668,chr(172)=>548,chr(173)=>440,chr(174)=>676,chr(175)=>413, + chr(176)=>278,chr(177)=>438,chr(178)=>328,chr(179)=>318,chr(180)=>449,chr(181)=>463,chr(182)=>535,chr(183)=>234,chr(184)=>535,chr(185)=>176,chr(186)=>322,chr(187)=>669,chr(188)=>761,chr(189)=>710,chr(190)=>904,chr(191)=>452,chr(192)=>511,chr(193)=>511,chr(194)=>514,chr(195)=>511,chr(196)=>511,chr(197)=>511, + chr(198)=>787,chr(199)=>480,chr(200)=>507,chr(201)=>507,chr(202)=>507,chr(203)=>507,chr(204)=>250,chr(205)=>250,chr(206)=>271,chr(207)=>272,chr(208)=>542,chr(209)=>548,chr(210)=>507,chr(211)=>507,chr(212)=>507,chr(213)=>507,chr(214)=>507,chr(215)=>496,chr(216)=>507,chr(217)=>527,chr(218)=>527,chr(219)=>526, + chr(220)=>527,chr(221)=>497,chr(222)=>522,chr(223)=>457,chr(224)=>446,chr(225)=>446,chr(226)=>446,chr(227)=>446,chr(228)=>446,chr(229)=>446,chr(230)=>635,chr(231)=>394,chr(232)=>404,chr(233)=>404,chr(234)=>412,chr(235)=>401,chr(236)=>235,chr(237)=>235,chr(238)=>256,chr(239)=>249,chr(240)=>475,chr(241)=>450, + chr(242)=>412,chr(243)=>412,chr(244)=>412,chr(245)=>413,chr(246)=>412,chr(247)=>444,chr(248)=>412,chr(249)=>458,chr(250)=>458,chr(251)=>458,chr(252)=>458,chr(253)=>429,chr(254)=>452,chr(255)=>429); +$enc = 'cp1252'; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +$file = 'CevicheOne-Regular.z'; +$originalsize = 25916; +$subsetted = true; +?> diff --git a/application/third_party/fpdf/tutorial/CevicheOne-Regular.ttf b/application/third_party/fpdf/tutorial/CevicheOne-Regular.ttf new file mode 100644 index 0000000..0bf3b6f Binary files /dev/null and b/application/third_party/fpdf/tutorial/CevicheOne-Regular.ttf differ diff --git a/application/third_party/fpdf/tutorial/CevicheOne-Regular.z b/application/third_party/fpdf/tutorial/CevicheOne-Regular.z new file mode 100644 index 0000000..a2c2325 Binary files /dev/null and b/application/third_party/fpdf/tutorial/CevicheOne-Regular.z differ diff --git a/application/third_party/fpdf/tutorial/countries.txt b/application/third_party/fpdf/tutorial/countries.txt new file mode 100644 index 0000000..aa8886c --- /dev/null +++ b/application/third_party/fpdf/tutorial/countries.txt @@ -0,0 +1,15 @@ +Austria;Vienna;83859;8075 +Belgium;Brussels;30518;10192 +Denmark;Copenhagen;43094;5295 +Finland;Helsinki;304529;5147 +France;Paris;543965;58728 +Germany;Berlin;357022;82057 +Greece;Athens;131625;10511 +Ireland;Dublin;70723;3694 +Italy;Roma;301316;57563 +Luxembourg;Luxembourg;2586;424 +Netherlands;Amsterdam;41526;15654 +Portugal;Lisbon;91906;9957 +Spain;Madrid;504790;39348 +Sweden;Stockholm;410934;8839 +United Kingdom;London;243820;58862 diff --git a/application/third_party/fpdf/tutorial/index.htm b/application/third_party/fpdf/tutorial/index.htm new file mode 100644 index 0000000..dfee8ba --- /dev/null +++ b/application/third_party/fpdf/tutorial/index.htm @@ -0,0 +1,20 @@ + + + + +Tutorials + + + +

Tutorials

+ + + diff --git a/application/third_party/fpdf/tutorial/logo.png b/application/third_party/fpdf/tutorial/logo.png new file mode 100644 index 0000000..284a007 Binary files /dev/null and b/application/third_party/fpdf/tutorial/logo.png differ diff --git a/application/third_party/fpdf/tutorial/makefont.php b/application/third_party/fpdf/tutorial/makefont.php new file mode 100644 index 0000000..4a883bc --- /dev/null +++ b/application/third_party/fpdf/tutorial/makefont.php @@ -0,0 +1,6 @@ + diff --git a/application/third_party/fpdf/tutorial/tuto1.htm b/application/third_party/fpdf/tutorial/tuto1.htm new file mode 100644 index 0000000..bb3036f --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto1.htm @@ -0,0 +1,76 @@ + + + + +Minimal example + + + +

Minimal example

+Let's start with the classic example: +
+
<?php
+require('fpdf.php');
+
+$pdf = new FPDF();
+$pdf->AddPage();
+$pdf->SetFont('Arial','B',16);
+$pdf->Cell(40,10,'Hello World!');
+$pdf->Output();
+?>
+
+

[Run]

+After including the library file, we create an FPDF object. +The constructor is used here with the default values: pages are in A4 portrait and +the unit of measure is millimeter. It could have been specified explicitly with: +
+
$pdf = new FPDF('P','mm','A4');
+
+
+It's possible to use landscape (L), other page sizes (such as Letter and +Legal) and units (pt, cm, in). +
+
+There's no page at the moment, so we have to add one with AddPage(). The origin +is at the upper-left corner and the current position is by default set at 1 cm from the +borders; the margins can be changed with SetMargins(). +
+
+Before we can print text, it's mandatory to select a font with SetFont(). +We choose Arial bold 16: +
+
$pdf->SetFont('Arial','B',16);
+
+
+We could have specified italics with I, underlined with U or a regular font with an empty string +(or any combination). Note that the font size is given in points, not millimeters (or another user +unit); it's the only exception. The other standard fonts are Times, Courier, Symbol and ZapfDingbats. +
+
+We can now print a cell with Cell(). A cell is a rectangular area, possibly framed, +which contains a line of text. It is output at the current position. We specify its dimensions, +its text (centered or aligned), if borders should be drawn, and where the current position +moves after it (to the right, below or to the beginning of the next line). To add a frame, we would do this: +
+
$pdf->Cell(40,10,'Hello World !',1);
+
+
+To add a new cell next to it with centered text and go to the next line, we would do: +
+
$pdf->Cell(60,10,'Powered by FPDF.',0,1,'C');
+
+
+Remark: the line break can also be done with Ln(). This method additionnaly allows to specify +the height of the break. +
+
+Finally, the document is closed and sent to the browser with Output(). We could have saved +it to a file by passing the appropriate parameters. +
+
+Caution: in case when the PDF is sent to the browser, nothing else must be output by the +script, neither before nor after (no HTML, not even a space or a carriage return). If you send something +before, you will get the error message: "Some data has already been output, can't send PDF file". If you +send something after, the document might not display. + + diff --git a/application/third_party/fpdf/tutorial/tuto1.php b/application/third_party/fpdf/tutorial/tuto1.php new file mode 100644 index 0000000..14a0504 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto1.php @@ -0,0 +1,9 @@ +AddPage(); +$pdf->SetFont('Arial','B',16); +$pdf->Cell(40,10,'Hello World!'); +$pdf->Output(); +?> diff --git a/application/third_party/fpdf/tutorial/tuto2.htm b/application/third_party/fpdf/tutorial/tuto2.htm new file mode 100644 index 0000000..dc65362 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto2.htm @@ -0,0 +1,80 @@ + + + + +Header, footer, page break and image + + + +

Header, footer, page break and image

+Here's a two page example with header, footer and logo: +
+
<?php
+require('fpdf.php');
+
+class PDF extends FPDF
+{
+// Page header
+function Header()
+{
+    // Logo
+    $this->Image('logo.png',10,6,30);
+    // Arial bold 15
+    $this->SetFont('Arial','B',15);
+    // Move to the right
+    $this->Cell(80);
+    // Title
+    $this->Cell(30,10,'Title',1,0,'C');
+    // Line break
+    $this->Ln(20);
+}
+
+// Page footer
+function Footer()
+{
+    // Position at 1.5 cm from bottom
+    $this->SetY(-15);
+    // Arial italic 8
+    $this->SetFont('Arial','I',8);
+    // Page number
+    $this->Cell(0,10,'Page '.$this->PageNo().'/{nb}',0,0,'C');
+}
+}
+
+// Instanciation of inherited class
+$pdf = new PDF();
+$pdf->AliasNbPages();
+$pdf->AddPage();
+$pdf->SetFont('Times','',12);
+for($i=1;$i<=40;$i++)
+    $pdf->Cell(0,10,'Printing line number '.$i,0,1);
+$pdf->Output();
+?>
+
+

[Run]

+This example makes use of the Header() and Footer() methods to process page headers and +footers. They are called automatically. They already exist in the FPDF class but do nothing, +therefore we have to extend the class and override them. +
+
+The logo is printed with the Image() method by specifying its upper-left corner and +its width. The height is calculated automatically to respect the image proportions. +
+
+To print the page number, a null value is passed as the cell width. It means that the cell +should extend up to the right margin of the page; this is handy to center text. The current page +number is returned by the PageNo() method; as for the total number of pages, it's obtained +via the special value {nb} which is substituted when the document is finished +(provided you first called AliasNbPages()). +
+Note the use of the SetY() method which allows to set position at an absolute location in +the page, starting from the top or the bottom. +
+
+Another interesting feature is used here: the automatic page breaking. As soon as a cell would +cross a limit in the page (at 2 centimeters from the bottom by default), a break is issued +and the font restored. Although the header and footer select their own font (Arial), the body +continues with Times. This mechanism of automatic restoration also applies to colors and line +width. The limit which triggers page breaks can be set with SetAutoPageBreak(). + + diff --git a/application/third_party/fpdf/tutorial/tuto2.php b/application/third_party/fpdf/tutorial/tuto2.php new file mode 100644 index 0000000..cc7d51c --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto2.php @@ -0,0 +1,41 @@ +Image('logo.png',10,6,30); + // Arial bold 15 + $this->SetFont('Arial','B',15); + // Move to the right + $this->Cell(80); + // Title + $this->Cell(30,10,'Title',1,0,'C'); + // Line break + $this->Ln(20); +} + +// Page footer +function Footer() +{ + // Position at 1.5 cm from bottom + $this->SetY(-15); + // Arial italic 8 + $this->SetFont('Arial','I',8); + // Page number + $this->Cell(0,10,'Page '.$this->PageNo().'/{nb}',0,0,'C'); +} +} + +// Instanciation of inherited class +$pdf = new PDF(); +$pdf->AliasNbPages(); +$pdf->AddPage(); +$pdf->SetFont('Times','',12); +for($i=1;$i<=40;$i++) + $pdf->Cell(0,10,'Printing line number '.$i,0,1); +$pdf->Output(); +?> diff --git a/application/third_party/fpdf/tutorial/tuto3.htm b/application/third_party/fpdf/tutorial/tuto3.htm new file mode 100644 index 0000000..e59267d --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto3.htm @@ -0,0 +1,115 @@ + + + + +Line breaks and colors + + + +

Line breaks and colors

+Let's continue with an example which prints justified paragraphs. It also illustrates the use +of colors. +
+
<?php
+require('fpdf.php');
+
+class PDF extends FPDF
+{
+function Header()
+{
+    global $title;
+
+    // Arial bold 15
+    $this->SetFont('Arial','B',15);
+    // Calculate width of title and position
+    $w = $this->GetStringWidth($title)+6;
+    $this->SetX((210-$w)/2);
+    // Colors of frame, background and text
+    $this->SetDrawColor(0,80,180);
+    $this->SetFillColor(230,230,0);
+    $this->SetTextColor(220,50,50);
+    // Thickness of frame (1 mm)
+    $this->SetLineWidth(1);
+    // Title
+    $this->Cell($w,9,$title,1,1,'C',true);
+    // Line break
+    $this->Ln(10);
+}
+
+function Footer()
+{
+    // Position at 1.5 cm from bottom
+    $this->SetY(-15);
+    // Arial italic 8
+    $this->SetFont('Arial','I',8);
+    // Text color in gray
+    $this->SetTextColor(128);
+    // Page number
+    $this->Cell(0,10,'Page '.$this->PageNo(),0,0,'C');
+}
+
+function ChapterTitle($num, $label)
+{
+    // Arial 12
+    $this->SetFont('Arial','',12);
+    // Background color
+    $this->SetFillColor(200,220,255);
+    // Title
+    $this->Cell(0,6,"Chapter $num : $label",0,1,'L',true);
+    // Line break
+    $this->Ln(4);
+}
+
+function ChapterBody($file)
+{
+    // Read text file
+    $txt = file_get_contents($file);
+    // Times 12
+    $this->SetFont('Times','',12);
+    // Output justified text
+    $this->MultiCell(0,5,$txt);
+    // Line break
+    $this->Ln();
+    // Mention in italics
+    $this->SetFont('','I');
+    $this->Cell(0,5,'(end of excerpt)');
+}
+
+function PrintChapter($num, $title, $file)
+{
+    $this->AddPage();
+    $this->ChapterTitle($num,$title);
+    $this->ChapterBody($file);
+}
+}
+
+$pdf = new PDF();
+$title = '20000 Leagues Under the Seas';
+$pdf->SetTitle($title);
+$pdf->SetAuthor('Jules Verne');
+$pdf->PrintChapter(1,'A RUNAWAY REEF','20k_c1.txt');
+$pdf->PrintChapter(2,'THE PROS AND CONS','20k_c2.txt');
+$pdf->Output();
+?>
+
+

[Run]

+The GetStringWidth() method allows to determine the length of a string in the current font, +which is used here to calculate the position and the width of the frame surrounding the title. +Then colors are set (via SetDrawColor(), SetFillColor() and SetTextColor()) and the +thickness of the line is set to 1 mm (instead of 0.2 by default) with SetLineWidth(). Finally, +we output the cell (the last parameter true indicates that the background must +be filled). +
+
+The method used to print the paragraphs is MultiCell(). Each time a line reaches the +right extremity of the cell or a carriage return character is met, a line break is issued +and a new cell automatically created under the current one. Text is justified by default. +
+
+Two document properties are defined: the title (SetTitle()) and the author (SetAuthor()). +There are several ways to view them in Adobe Reader. The first one is to open the file directly with +the reader, go to the File menu and choose the Properties option. The second one, also available from +the plug-in, is to right-click and select Document Properties. The third method is to type the Ctrl+D +key combination. + + diff --git a/application/third_party/fpdf/tutorial/tuto3.php b/application/third_party/fpdf/tutorial/tuto3.php new file mode 100644 index 0000000..eade51c --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto3.php @@ -0,0 +1,81 @@ +SetFont('Arial','B',15); + // Calculate width of title and position + $w = $this->GetStringWidth($title)+6; + $this->SetX((210-$w)/2); + // Colors of frame, background and text + $this->SetDrawColor(0,80,180); + $this->SetFillColor(230,230,0); + $this->SetTextColor(220,50,50); + // Thickness of frame (1 mm) + $this->SetLineWidth(1); + // Title + $this->Cell($w,9,$title,1,1,'C',true); + // Line break + $this->Ln(10); +} + +function Footer() +{ + // Position at 1.5 cm from bottom + $this->SetY(-15); + // Arial italic 8 + $this->SetFont('Arial','I',8); + // Text color in gray + $this->SetTextColor(128); + // Page number + $this->Cell(0,10,'Page '.$this->PageNo(),0,0,'C'); +} + +function ChapterTitle($num, $label) +{ + // Arial 12 + $this->SetFont('Arial','',12); + // Background color + $this->SetFillColor(200,220,255); + // Title + $this->Cell(0,6,"Chapter $num : $label",0,1,'L',true); + // Line break + $this->Ln(4); +} + +function ChapterBody($file) +{ + // Read text file + $txt = file_get_contents($file); + // Times 12 + $this->SetFont('Times','',12); + // Output justified text + $this->MultiCell(0,5,$txt); + // Line break + $this->Ln(); + // Mention in italics + $this->SetFont('','I'); + $this->Cell(0,5,'(end of excerpt)'); +} + +function PrintChapter($num, $title, $file) +{ + $this->AddPage(); + $this->ChapterTitle($num,$title); + $this->ChapterBody($file); +} +} + +$pdf = new PDF(); +$title = '20000 Leagues Under the Seas'; +$pdf->SetTitle($title); +$pdf->SetAuthor('Jules Verne'); +$pdf->PrintChapter(1,'A RUNAWAY REEF','20k_c1.txt'); +$pdf->PrintChapter(2,'THE PROS AND CONS','20k_c2.txt'); +$pdf->Output(); +?> diff --git a/application/third_party/fpdf/tutorial/tuto4.htm b/application/third_party/fpdf/tutorial/tuto4.htm new file mode 100644 index 0000000..6432187 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto4.htm @@ -0,0 +1,132 @@ + + + + +Multi-columns + + + +

Multi-columns

+This example is a variant of the previous one showing how to lay the text across multiple +columns. +
+
<?php
+require('fpdf.php');
+
+class PDF extends FPDF
+{
+protected $col = 0; // Current column
+protected $y0;      // Ordinate of column start
+
+function Header()
+{
+    // Page header
+    global $title;
+
+    $this->SetFont('Arial','B',15);
+    $w = $this->GetStringWidth($title)+6;
+    $this->SetX((210-$w)/2);
+    $this->SetDrawColor(0,80,180);
+    $this->SetFillColor(230,230,0);
+    $this->SetTextColor(220,50,50);
+    $this->SetLineWidth(1);
+    $this->Cell($w,9,$title,1,1,'C',true);
+    $this->Ln(10);
+    // Save ordinate
+    $this->y0 = $this->GetY();
+}
+
+function Footer()
+{
+    // Page footer
+    $this->SetY(-15);
+    $this->SetFont('Arial','I',8);
+    $this->SetTextColor(128);
+    $this->Cell(0,10,'Page '.$this->PageNo(),0,0,'C');
+}
+
+function SetCol($col)
+{
+    // Set position at a given column
+    $this->col = $col;
+    $x = 10+$col*65;
+    $this->SetLeftMargin($x);
+    $this->SetX($x);
+}
+
+function AcceptPageBreak()
+{
+    // Method accepting or not automatic page break
+    if($this->col<2)
+    {
+        // Go to next column
+        $this->SetCol($this->col+1);
+        // Set ordinate to top
+        $this->SetY($this->y0);
+        // Keep on page
+        return false;
+    }
+    else
+    {
+        // Go back to first column
+        $this->SetCol(0);
+        // Page break
+        return true;
+    }
+}
+
+function ChapterTitle($num, $label)
+{
+    // Title
+    $this->SetFont('Arial','',12);
+    $this->SetFillColor(200,220,255);
+    $this->Cell(0,6,"Chapter $num : $label",0,1,'L',true);
+    $this->Ln(4);
+    // Save ordinate
+    $this->y0 = $this->GetY();
+}
+
+function ChapterBody($file)
+{
+    // Read text file
+    $txt = file_get_contents($file);
+    // Font
+    $this->SetFont('Times','',12);
+    // Output text in a 6 cm width column
+    $this->MultiCell(60,5,$txt);
+    $this->Ln();
+    // Mention
+    $this->SetFont('','I');
+    $this->Cell(0,5,'(end of excerpt)');
+    // Go back to first column
+    $this->SetCol(0);
+}
+
+function PrintChapter($num, $title, $file)
+{
+    // Add chapter
+    $this->AddPage();
+    $this->ChapterTitle($num,$title);
+    $this->ChapterBody($file);
+}
+}
+
+$pdf = new PDF();
+$title = '20000 Leagues Under the Seas';
+$pdf->SetTitle($title);
+$pdf->SetAuthor('Jules Verne');
+$pdf->PrintChapter(1,'A RUNAWAY REEF','20k_c1.txt');
+$pdf->PrintChapter(2,'THE PROS AND CONS','20k_c2.txt');
+$pdf->Output();
+?>
+
+

[Run]

+The key method used is AcceptPageBreak(). It allows to accept or not an automatic page +break. By refusing it and altering the margin and current position, the desired column layout +is achieved. +
+For the rest, not many changes; two properties have been added to the class to save the current +column number and the position where columns begin, and the MultiCell() call specifies a +6 centimeter width. + + diff --git a/application/third_party/fpdf/tutorial/tuto4.php b/application/third_party/fpdf/tutorial/tuto4.php new file mode 100644 index 0000000..f5f4fa8 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto4.php @@ -0,0 +1,109 @@ +SetFont('Arial','B',15); + $w = $this->GetStringWidth($title)+6; + $this->SetX((210-$w)/2); + $this->SetDrawColor(0,80,180); + $this->SetFillColor(230,230,0); + $this->SetTextColor(220,50,50); + $this->SetLineWidth(1); + $this->Cell($w,9,$title,1,1,'C',true); + $this->Ln(10); + // Save ordinate + $this->y0 = $this->GetY(); +} + +function Footer() +{ + // Page footer + $this->SetY(-15); + $this->SetFont('Arial','I',8); + $this->SetTextColor(128); + $this->Cell(0,10,'Page '.$this->PageNo(),0,0,'C'); +} + +function SetCol($col) +{ + // Set position at a given column + $this->col = $col; + $x = 10+$col*65; + $this->SetLeftMargin($x); + $this->SetX($x); +} + +function AcceptPageBreak() +{ + // Method accepting or not automatic page break + if($this->col<2) + { + // Go to next column + $this->SetCol($this->col+1); + // Set ordinate to top + $this->SetY($this->y0); + // Keep on page + return false; + } + else + { + // Go back to first column + $this->SetCol(0); + // Page break + return true; + } +} + +function ChapterTitle($num, $label) +{ + // Title + $this->SetFont('Arial','',12); + $this->SetFillColor(200,220,255); + $this->Cell(0,6,"Chapter $num : $label",0,1,'L',true); + $this->Ln(4); + // Save ordinate + $this->y0 = $this->GetY(); +} + +function ChapterBody($file) +{ + // Read text file + $txt = file_get_contents($file); + // Font + $this->SetFont('Times','',12); + // Output text in a 6 cm width column + $this->MultiCell(60,5,$txt); + $this->Ln(); + // Mention + $this->SetFont('','I'); + $this->Cell(0,5,'(end of excerpt)'); + // Go back to first column + $this->SetCol(0); +} + +function PrintChapter($num, $title, $file) +{ + // Add chapter + $this->AddPage(); + $this->ChapterTitle($num,$title); + $this->ChapterBody($file); +} +} + +$pdf = new PDF(); +$title = '20000 Leagues Under the Seas'; +$pdf->SetTitle($title); +$pdf->SetAuthor('Jules Verne'); +$pdf->PrintChapter(1,'A RUNAWAY REEF','20k_c1.txt'); +$pdf->PrintChapter(2,'THE PROS AND CONS','20k_c2.txt'); +$pdf->Output(); +?> diff --git a/application/third_party/fpdf/tutorial/tuto5.htm b/application/third_party/fpdf/tutorial/tuto5.htm new file mode 100644 index 0000000..1f992c7 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto5.htm @@ -0,0 +1,134 @@ + + + + +Tables + + + +

Tables

+This tutorial shows different ways to make tables. +
+
<?php
+require('fpdf.php');
+
+class PDF extends FPDF
+{
+// Load data
+function LoadData($file)
+{
+    // Read file lines
+    $lines = file($file);
+    $data = array();
+    foreach($lines as $line)
+        $data[] = explode(';',trim($line));
+    return $data;
+}
+
+// Simple table
+function BasicTable($header, $data)
+{
+    // Header
+    foreach($header as $col)
+        $this->Cell(40,7,$col,1);
+    $this->Ln();
+    // Data
+    foreach($data as $row)
+    {
+        foreach($row as $col)
+            $this->Cell(40,6,$col,1);
+        $this->Ln();
+    }
+}
+
+// Better table
+function ImprovedTable($header, $data)
+{
+    // Column widths
+    $w = array(40, 35, 40, 45);
+    // Header
+    for($i=0;$i<count($header);$i++)
+        $this->Cell($w[$i],7,$header[$i],1,0,'C');
+    $this->Ln();
+    // Data
+    foreach($data as $row)
+    {
+        $this->Cell($w[0],6,$row[0],'LR');
+        $this->Cell($w[1],6,$row[1],'LR');
+        $this->Cell($w[2],6,number_format($row[2]),'LR',0,'R');
+        $this->Cell($w[3],6,number_format($row[3]),'LR',0,'R');
+        $this->Ln();
+    }
+    // Closing line
+    $this->Cell(array_sum($w),0,'','T');
+}
+
+// Colored table
+function FancyTable($header, $data)
+{
+    // Colors, line width and bold font
+    $this->SetFillColor(255,0,0);
+    $this->SetTextColor(255);
+    $this->SetDrawColor(128,0,0);
+    $this->SetLineWidth(.3);
+    $this->SetFont('','B');
+    // Header
+    $w = array(40, 35, 40, 45);
+    for($i=0;$i<count($header);$i++)
+        $this->Cell($w[$i],7,$header[$i],1,0,'C',true);
+    $this->Ln();
+    // Color and font restoration
+    $this->SetFillColor(224,235,255);
+    $this->SetTextColor(0);
+    $this->SetFont('');
+    // Data
+    $fill = false;
+    foreach($data as $row)
+    {
+        $this->Cell($w[0],6,$row[0],'LR',0,'L',$fill);
+        $this->Cell($w[1],6,$row[1],'LR',0,'L',$fill);
+        $this->Cell($w[2],6,number_format($row[2]),'LR',0,'R',$fill);
+        $this->Cell($w[3],6,number_format($row[3]),'LR',0,'R',$fill);
+        $this->Ln();
+        $fill = !$fill;
+    }
+    // Closing line
+    $this->Cell(array_sum($w),0,'','T');
+}
+}
+
+$pdf = new PDF();
+// Column headings
+$header = array('Country', 'Capital', 'Area (sq km)', 'Pop. (thousands)');
+// Data loading
+$data = $pdf->LoadData('countries.txt');
+$pdf->SetFont('Arial','',14);
+$pdf->AddPage();
+$pdf->BasicTable($header,$data);
+$pdf->AddPage();
+$pdf->ImprovedTable($header,$data);
+$pdf->AddPage();
+$pdf->FancyTable($header,$data);
+$pdf->Output();
+?>
+
+

[Run]

+A table being just a collection of cells, it's natural to build one from them. The first +example is achieved in the most basic way possible: simple framed cells, all of the same size +and left aligned. The result is rudimentary but very quick to obtain. +
+
+The second table brings some improvements: each column has its own width, headings are centered, +and numbers right aligned. Moreover, horizontal lines have been removed. This is done by means +of the border parameter of the Cell() method, which specifies which sides of the +cell must be drawn. Here we want the left (L) and right (R) ones. It remains +the problem of the horizontal line to finish the table. There are two possibilities: either +check for the last line in the loop, in which case we use LRB for the border +parameter; or, as done here, add the line once the loop is over. +
+
+The third table is similar to the second one but uses colors. Fill, text and line colors are +simply specified. Alternate coloring for rows is obtained by using alternatively transparent +and filled cells. + + diff --git a/application/third_party/fpdf/tutorial/tuto5.php b/application/third_party/fpdf/tutorial/tuto5.php new file mode 100644 index 0000000..f1b64a2 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto5.php @@ -0,0 +1,102 @@ +Cell(40,7,$col,1); + $this->Ln(); + // Data + foreach($data as $row) + { + foreach($row as $col) + $this->Cell(40,6,$col,1); + $this->Ln(); + } +} + +// Better table +function ImprovedTable($header, $data) +{ + // Column widths + $w = array(40, 35, 40, 45); + // Header + for($i=0;$iCell($w[$i],7,$header[$i],1,0,'C'); + $this->Ln(); + // Data + foreach($data as $row) + { + $this->Cell($w[0],6,$row[0],'LR'); + $this->Cell($w[1],6,$row[1],'LR'); + $this->Cell($w[2],6,number_format($row[2]),'LR',0,'R'); + $this->Cell($w[3],6,number_format($row[3]),'LR',0,'R'); + $this->Ln(); + } + // Closing line + $this->Cell(array_sum($w),0,'','T'); +} + +// Colored table +function FancyTable($header, $data) +{ + // Colors, line width and bold font + $this->SetFillColor(255,0,0); + $this->SetTextColor(255); + $this->SetDrawColor(128,0,0); + $this->SetLineWidth(.3); + $this->SetFont('','B'); + // Header + $w = array(40, 35, 40, 45); + for($i=0;$iCell($w[$i],7,$header[$i],1,0,'C',true); + $this->Ln(); + // Color and font restoration + $this->SetFillColor(224,235,255); + $this->SetTextColor(0); + $this->SetFont(''); + // Data + $fill = false; + foreach($data as $row) + { + $this->Cell($w[0],6,$row[0],'LR',0,'L',$fill); + $this->Cell($w[1],6,$row[1],'LR',0,'L',$fill); + $this->Cell($w[2],6,number_format($row[2]),'LR',0,'R',$fill); + $this->Cell($w[3],6,number_format($row[3]),'LR',0,'R',$fill); + $this->Ln(); + $fill = !$fill; + } + // Closing line + $this->Cell(array_sum($w),0,'','T'); +} +} + +$pdf = new PDF(); +// Column headings +$header = array('Country', 'Capital', 'Area (sq km)', 'Pop. (thousands)'); +// Data loading +$data = $pdf->LoadData('countries.txt'); +$pdf->SetFont('Arial','',14); +$pdf->AddPage(); +$pdf->BasicTable($header,$data); +$pdf->AddPage(); +$pdf->ImprovedTable($header,$data); +$pdf->AddPage(); +$pdf->FancyTable($header,$data); +$pdf->Output(); +?> diff --git a/application/third_party/fpdf/tutorial/tuto6.htm b/application/third_party/fpdf/tutorial/tuto6.htm new file mode 100644 index 0000000..a11e355 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto6.htm @@ -0,0 +1,154 @@ + + + + +Links and flowing text + + + +

Links and flowing text

+This tutorial explains how to insert links (internal and external) and shows a new text writing +mode. It also contains a basic HTML parser. +
+
<?php
+require('fpdf.php');
+
+class PDF extends FPDF
+{
+protected $B = 0;
+protected $I = 0;
+protected $U = 0;
+protected $HREF = '';
+
+function WriteHTML($html)
+{
+    // HTML parser
+    $html = str_replace("\n",' ',$html);
+    $a = preg_split('/<(.*)>/U',$html,-1,PREG_SPLIT_DELIM_CAPTURE);
+    foreach($a as $i=>$e)
+    {
+        if($i%2==0)
+        {
+            // Text
+            if($this->HREF)
+                $this->PutLink($this->HREF,$e);
+            else
+                $this->Write(5,$e);
+        }
+        else
+        {
+            // Tag
+            if($e[0]=='/')
+                $this->CloseTag(strtoupper(substr($e,1)));
+            else
+            {
+                // Extract attributes
+                $a2 = explode(' ',$e);
+                $tag = strtoupper(array_shift($a2));
+                $attr = array();
+                foreach($a2 as $v)
+                {
+                    if(preg_match('/([^=]*)=["\']?([^"\']*)/',$v,$a3))
+                        $attr[strtoupper($a3[1])] = $a3[2];
+                }
+                $this->OpenTag($tag,$attr);
+            }
+        }
+    }
+}
+
+function OpenTag($tag, $attr)
+{
+    // Opening tag
+    if($tag=='B' || $tag=='I' || $tag=='U')
+        $this->SetStyle($tag,true);
+    if($tag=='A')
+        $this->HREF = $attr['HREF'];
+    if($tag=='BR')
+        $this->Ln(5);
+}
+
+function CloseTag($tag)
+{
+    // Closing tag
+    if($tag=='B' || $tag=='I' || $tag=='U')
+        $this->SetStyle($tag,false);
+    if($tag=='A')
+        $this->HREF = '';
+}
+
+function SetStyle($tag, $enable)
+{
+    // Modify style and select corresponding font
+    $this->$tag += ($enable ? 1 : -1);
+    $style = '';
+    foreach(array('B', 'I', 'U') as $s)
+    {
+        if($this->$s>0)
+            $style .= $s;
+    }
+    $this->SetFont('',$style);
+}
+
+function PutLink($URL, $txt)
+{
+    // Put a hyperlink
+    $this->SetTextColor(0,0,255);
+    $this->SetStyle('U',true);
+    $this->Write(5,$txt,$URL);
+    $this->SetStyle('U',false);
+    $this->SetTextColor(0);
+}
+}
+
+$html = 'You can now easily print text mixing different styles: <b>bold</b>, <i>italic</i>,
+<u>underlined</u>, or <b><i><u>all at once</u></i></b>!<br><br>You can also insert links on
+text, such as <a href="http://www.fpdf.org">www.fpdf.org</a>, or on an image: click on the logo.';
+
+$pdf = new PDF();
+// First page
+$pdf->AddPage();
+$pdf->SetFont('Arial','',20);
+$pdf->Write(5,"To find out what's new in this tutorial, click ");
+$pdf->SetFont('','U');
+$link = $pdf->AddLink();
+$pdf->Write(5,'here',$link);
+$pdf->SetFont('');
+// Second page
+$pdf->AddPage();
+$pdf->SetLink($link);
+$pdf->Image('logo.png',10,12,30,0,'','http://www.fpdf.org');
+$pdf->SetLeftMargin(45);
+$pdf->SetFontSize(14);
+$pdf->WriteHTML($html);
+$pdf->Output();
+?>
+
+

[Run]

+The new method to print text is Write(). It's very close to MultiCell(); the differences are: +
    +
  • The end of line is at the right margin and the next line begins at the left one
  • +
  • The current position moves at the end of the text
  • +
+So it allows to write a chunk of text, alter the font style, then continue from the exact +place we left it. On the other hand, you cannot justify it. +
+
+The method is used on the first page to put a link pointing to the second one. The beginning of +the sentence is written in regular style, then we switch to underline and finish it. The link +is created with AddLink(), which returns a link identifier. The identifier is +passed as third parameter of Write(). Once the second page is created, we use SetLink() to +make the link point to the beginning of the current page. +
+
+Then we put an image with an external link on it. An external link is just a URL. It's passed as +last parameter of Image(). +
+
+Finally, the left margin is moved after the image with SetLeftMargin() and some text in +HTML format is output. A very simple HTML parser is used for this, based on regular expressions. +Recognized tags are <b>, <i>, <u>, <a> and <br>; the others are +ignored. The parser also makes use of the Write() method. An external link is put the same way as +an internal one (third parameter of Write()). Note that Cell() also allows to put links. + + diff --git a/application/third_party/fpdf/tutorial/tuto6.php b/application/third_party/fpdf/tutorial/tuto6.php new file mode 100644 index 0000000..66580e9 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto6.php @@ -0,0 +1,113 @@ +/U',$html,-1,PREG_SPLIT_DELIM_CAPTURE); + foreach($a as $i=>$e) + { + if($i%2==0) + { + // Text + if($this->HREF) + $this->PutLink($this->HREF,$e); + else + $this->Write(5,$e); + } + else + { + // Tag + if($e[0]=='/') + $this->CloseTag(strtoupper(substr($e,1))); + else + { + // Extract attributes + $a2 = explode(' ',$e); + $tag = strtoupper(array_shift($a2)); + $attr = array(); + foreach($a2 as $v) + { + if(preg_match('/([^=]*)=["\']?([^"\']*)/',$v,$a3)) + $attr[strtoupper($a3[1])] = $a3[2]; + } + $this->OpenTag($tag,$attr); + } + } + } +} + +function OpenTag($tag, $attr) +{ + // Opening tag + if($tag=='B' || $tag=='I' || $tag=='U') + $this->SetStyle($tag,true); + if($tag=='A') + $this->HREF = $attr['HREF']; + if($tag=='BR') + $this->Ln(5); +} + +function CloseTag($tag) +{ + // Closing tag + if($tag=='B' || $tag=='I' || $tag=='U') + $this->SetStyle($tag,false); + if($tag=='A') + $this->HREF = ''; +} + +function SetStyle($tag, $enable) +{ + // Modify style and select corresponding font + $this->$tag += ($enable ? 1 : -1); + $style = ''; + foreach(array('B', 'I', 'U') as $s) + { + if($this->$s>0) + $style .= $s; + } + $this->SetFont('',$style); +} + +function PutLink($URL, $txt) +{ + // Put a hyperlink + $this->SetTextColor(0,0,255); + $this->SetStyle('U',true); + $this->Write(5,$txt,$URL); + $this->SetStyle('U',false); + $this->SetTextColor(0); +} +} + +$html = 'You can now easily print text mixing different styles: bold, italic, +underlined, or all at once!

You can also insert links on +text, such as www.fpdf.org, or on an image: click on the logo.'; + +$pdf = new PDF(); +// First page +$pdf->AddPage(); +$pdf->SetFont('Arial','',20); +$pdf->Write(5,"To find out what's new in this tutorial, click "); +$pdf->SetFont('','U'); +$link = $pdf->AddLink(); +$pdf->Write(5,'here',$link); +$pdf->SetFont(''); +// Second page +$pdf->AddPage(); +$pdf->SetLink($link); +$pdf->Image('logo.png',10,12,30,0,'','http://www.fpdf.org'); +$pdf->SetLeftMargin(45); +$pdf->SetFontSize(14); +$pdf->WriteHTML($html); +$pdf->Output(); +?> diff --git a/application/third_party/fpdf/tutorial/tuto7.htm b/application/third_party/fpdf/tutorial/tuto7.htm new file mode 100644 index 0000000..91e8dd1 --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto7.htm @@ -0,0 +1,182 @@ + + + + +Adding new fonts and encodings + + + +

Adding new fonts and encodings

+This tutorial explains how to use TrueType, OpenType and Type1 fonts so that you are not limited to the +standard fonts anymore. The other benefit is that you can choose the text encoding, which allows you to +use other languages than the Western ones (the standard fonts support only cp1252 aka windows-1252). +
+
+For OpenType, only the format based on TrueType is supported (not the one based on Type1).
+For Type1, you will need the corresponding AFM file (it is usually provided with the font). +
+
+Adding a new font requires two steps: +
    +
  • Generation of the font definition file
  • +
  • Declaration of the font in the script
  • +
+ +

Generation of the font definition file

+The first step consists in generating a PHP file containing all the information needed by FPDF; +in addition, the font file is compressed. To do this, a helper script is provided in the makefont +directory of the package: makefont.php. It contains the following function: +
+
+MakeFont(string fontfile [, string enc [, boolean embed [, boolean subset]]]) +
+
fontfile
+
+

Path to the .ttf, .otf or .pfb file.

+
+
enc
+
+

Name of the encoding to use. Default value: cp1252.

+
+
embed
+
+

Whether to embed the font or not. Default value: true.

+
+
subset
+
+

Whether to subset the font or not. Default value: true.

+
+
+The first parameter is the name of the font file. The extension must be either .ttf, .otf or .pfb and +determines the font type. If your Type1 font is in ASCII format (.pfa), you can convert it to binary +(.pfb) with the help of t1utils. +
+
+For Type1 fonts, the corresponding .afm file must be present in the same directory. +
+
+The encoding defines the association between a code (from 0 to 255) and a character. The first 128 are +always the same and correspond to ASCII; the following are variable. Encodings are stored in .map +files. The available ones are: +
    +
  • cp1250 (Central Europe)
  • +
  • cp1251 (Cyrillic)
  • +
  • cp1252 (Western Europe)
  • +
  • cp1253 (Greek)
  • +
  • cp1254 (Turkish)
  • +
  • cp1255 (Hebrew)
  • +
  • cp1257 (Baltic)
  • +
  • cp1258 (Vietnamese)
  • +
  • cp874 (Thai)
  • +
  • ISO-8859-1 (Western Europe)
  • +
  • ISO-8859-2 (Central Europe)
  • +
  • ISO-8859-4 (Baltic)
  • +
  • ISO-8859-5 (Cyrillic)
  • +
  • ISO-8859-7 (Greek)
  • +
  • ISO-8859-9 (Turkish)
  • +
  • ISO-8859-11 (Thai)
  • +
  • ISO-8859-15 (Western Europe)
  • +
  • ISO-8859-16 (Central Europe)
  • +
  • KOI8-R (Russian)
  • +
  • KOI8-U (Ukrainian)
  • +
+Of course, the font must contain the characters corresponding to the selected encoding. +
+
+The third parameter indicates whether the font should be embedded in the PDF or not. When a font is +not embedded, it is searched in the system. The advantage is that the PDF file is smaller; on the +other hand, if it is not available, then a substitution font is used. So you should ensure that the +needed font is installed on the client systems. Embedding is the recommended option to guarantee a +correct rendering. +
+
+The last parameter indicates whether subsetting should be used, that is to say, whether only +the characters from the selected encoding should be kept in the embedded font. As a result, +the size of the PDF file can be greatly reduced, especially if the original font was big. +
+
+After you have called the function (create a new file for this and include makefont.php), a .php file +is created, with the same name as the font file. You may rename it if you wish. If the case of embedding, +the font file is compressed and gives a second file with .z as extension (except if the compression +function is not available, it requires Zlib). You may rename it too, but in this case you have to change +the variable $file in the .php file accordingly. +
+
+Example: +
+
<?php
+require('makefont/makefont.php');
+
+MakeFont('C:\\Windows\\Fonts\\comic.ttf','cp1252');
+?>
+
+which gives the files comic.php and comic.z. +
+
+Then copy the generated files to the font directory. If the font file could not be compressed, copy +it directly instead of the .z version. +
+
+Another way to call MakeFont() is through the command line: +
+
+php makefont\makefont.php C:\Windows\Fonts\comic.ttf cp1252 +
+
+Finally, for TrueType and OpenType fonts, you can also generate the files +online instead of doing it manually. + +

Declaration of the font in the script

+The second step is simple. You just need to call the AddFont() method: +
+
$pdf->AddFont('Comic','','comic.php');
+
+
+And the font is now available (in regular and underlined styles), usable like the others. If we +had worked with Comic Sans MS Bold (comicbd.ttf), we would have written: +
+
$pdf->AddFont('Comic','B','comicbd.php');
+
+
+ +

Example

+Now let's see a complete example. We will use the Ceviche One font. +The first step is the generation of the font files: +
+
<?php
+require('makefont/makefont.php');
+
+MakeFont('CevicheOne-Regular.ttf','cp1252');
+?>
+
+The script produces the following output: +
+
+Font file compressed: CevicheOne-Regular.z
+Font definition file generated: CevicheOne-Regular.php
+
+Alternatively we could have used the command line: +
+
+php makefont\makefont.php CevicheOne-Regular.ttf cp1252 +
+
+or used the online generator. +
+
+We can now copy the two generated files to the font directory and write the script: +
+
<?php
+require('fpdf.php');
+
+$pdf = new FPDF();
+$pdf->AddFont('CevicheOne','','CevicheOne-Regular.php');
+$pdf->AddPage();
+$pdf->SetFont('CevicheOne','',45);
+$pdf->Write(10,'Enjoy new fonts with FPDF!');
+$pdf->Output();
+?>
+
+

[Run]

+ + diff --git a/application/third_party/fpdf/tutorial/tuto7.php b/application/third_party/fpdf/tutorial/tuto7.php new file mode 100644 index 0000000..260cd0f --- /dev/null +++ b/application/third_party/fpdf/tutorial/tuto7.php @@ -0,0 +1,10 @@ +AddFont('CevicheOne','','CevicheOne-Regular.php','.'); +$pdf->AddPage(); +$pdf->SetFont('CevicheOne','',45); +$pdf->Write(10,'Enjoy new fonts with FPDF!'); +$pdf->Output(); +?> diff --git a/application/third_party/index.html b/application/third_party/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/third_party/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/accounts/layout.php b/application/views/accounts/layout.php new file mode 100644 index 0000000..b6c82de --- /dev/null +++ b/application/views/accounts/layout.php @@ -0,0 +1,215 @@ +
+
+
+ +
+
Chart of Account (COA)
+ +
+ +
+ + + + + + + + + + + + + +
NoKodeNamaTipePosisiKategoriAksi
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/application/views/asset/layout.php b/application/views/asset/layout.php new file mode 100644 index 0000000..34728f7 --- /dev/null +++ b/application/views/asset/layout.php @@ -0,0 +1,527 @@ +
+
+
+ +
+
Data Asset
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + +
NoTanggalKodeNamaLokasi AssetNilaiMasaSusut/BulanCreateAksi
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/views/auth/login.php b/application/views/auth/login.php new file mode 100644 index 0000000..08df524 --- /dev/null +++ b/application/views/auth/login.php @@ -0,0 +1,146 @@ + + + + + + + Login - Accounting + + + + + + + + + + + + + + + + + + + + + + + + + + +session->flashdata('error')): ?> + + \ No newline at end of file diff --git a/application/views/basejurnal/layout.php b/application/views/basejurnal/layout.php new file mode 100644 index 0000000..8fbbf02 --- /dev/null +++ b/application/views/basejurnal/layout.php @@ -0,0 +1,339 @@ +
+
+
+ +
+
Base Jurnal
+ +
+ +
+ + + + + + + + + + + + +
NoKodeNamaDeskripsiStatusAksi
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/application/views/buku_besar/layout.php b/application/views/buku_besar/layout.php new file mode 100644 index 0000000..eca4e7c --- /dev/null +++ b/application/views/buku_besar/layout.php @@ -0,0 +1,101 @@ +
+
+
+ +
+
Buku Besar
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ + + + + + + + + + + + +
TanggalNo RefKeteranganDebitKreditSaldo
+
+ +
+
+
+ + \ No newline at end of file diff --git a/application/views/card/preview.php b/application/views/card/preview.php new file mode 100644 index 0000000..a6490ad --- /dev/null +++ b/application/views/card/preview.php @@ -0,0 +1,68 @@ + + +
+
+

+

NIS:

+

Kelas:

+
+
+ +
+ +
diff --git a/application/views/customers/layout.php b/application/views/customers/layout.php new file mode 100644 index 0000000..b57795c --- /dev/null +++ b/application/views/customers/layout.php @@ -0,0 +1,165 @@ +
+
+
+ +
+
Data Customers
+ +
+ +
+ + + + + + + + + + + +
NoNamaAlamatTelpEmailAksi
+
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/application/views/dashboard/activity_logs.php b/application/views/dashboard/activity_logs.php new file mode 100644 index 0000000..4ed1dc6 --- /dev/null +++ b/application/views/dashboard/activity_logs.php @@ -0,0 +1,102 @@ +
+
+
+ +
+
Activity Log
+ +
+ +
+ + + + + + + + + + + + +
WaktuUserModuleActionDeskripsiStatusIP
+
+ +
+
+
+ + \ No newline at end of file diff --git a/application/views/dashboard/layout.php b/application/views/dashboard/layout.php new file mode 100644 index 0000000..0425ef1 --- /dev/null +++ b/application/views/dashboard/layout.php @@ -0,0 +1,849 @@ +session->userdata('role'); ?> + + + + +
+ + + +
+
+
+
+
+
+
Rp
+ Total Pemasukan +
+
+
+
+ +
+
+
+
+
+
Rp
+ Total Pengeluaran +
+
+
+
+ +
+
+
+
+
+
Rp
+ Keuntungan Bersih +
+
+
+
+ +
+ + + + + + + +
+ +
+
+
+
+
+

Ringkasan Keuangan

+ Performa keuangan +
+
+ +
+
+
+ +
+
+
+
+ + +
+
+
+ + +
+
+ +
+
+
AI Insight
+ Rekomendasi otomatis sistem +
+
+ + +
+ + + +
+ +
Belum ada insight hari ini
+
+ + + + + + severity == 'critical'){ + $icon = 'bi-x-octagon-fill'; + $colorClass = 'ai-danger'; + } elseif($ai->severity == 'warning'){ + $icon = 'bi-exclamation-triangle-fill'; + $colorClass = 'ai-warning'; + } elseif($ai->severity == 'info'){ + $icon = 'bi-info-circle-fill'; + $colorClass = 'ai-info'; + } elseif($ai->type == 'stock'){ + $icon = 'bi-box-seam-fill'; + $colorClass = 'ai-stock'; + } elseif($ai->type == 'finance'){ + $icon = 'bi-cash-coin'; + $colorClass = 'ai-finance'; + } + ?> + +
+ +
+ + + +
+
+ message; ?> +
+ + meta)): ?> +
+ + detail tambahan tersedia +
+ +
+ +
+ +
+ + + + + +
+
+
+
+
+ + + + +
+
+
+
+
+
+

Aktivitas Terbaru

+ Aktivitas terakhir di sistem +
+ + Lihat Semua + +
+ +
+ + + + + + + + + + + +
WaktuModulKeteranganPenggunaStatus
+
+ +
+
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/application/views/employees/attendance_monitoring.php b/application/views/employees/attendance_monitoring.php new file mode 100644 index 0000000..1f6bd15 --- /dev/null +++ b/application/views/employees/attendance_monitoring.php @@ -0,0 +1,75 @@ +
+ +
+ +
+ +
+ +
+ Monitoring Absensi +
+ + + +
+ +
+ + + + + + + + + + + + + + + +
NoKodeNamaShiftCheckinCheckoutStatus
+ +
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/application/views/employees/employees.php b/application/views/employees/employees.php new file mode 100644 index 0000000..40685e0 --- /dev/null +++ b/application/views/employees/employees.php @@ -0,0 +1,1341 @@ +
+ +
+ +
+ +
+ +
+

+ Data Karyawan +

+ + + Management Data Karyawan + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
NoFotoKodeFingerprintNamaDepartmentPositionBranchPhoneStatusActiveAksi
+ +
+ +
+ +
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/application/views/employees/generate_schedule.php b/application/views/employees/generate_schedule.php new file mode 100644 index 0000000..818b943 --- /dev/null +++ b/application/views/employees/generate_schedule.php @@ -0,0 +1,1059 @@ + + + + +
+ + +
+ +
+ +
+ +
+ +
+ Schedule Planner +
+ +
+ Jadwal kerja harian karyawan +
+ +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + + + + + \ No newline at end of file diff --git a/application/views/employees/holidays.php b/application/views/employees/holidays.php new file mode 100644 index 0000000..64679e7 --- /dev/null +++ b/application/views/employees/holidays.php @@ -0,0 +1,334 @@ +
+ +
+ +
+ +
+ +
+ Master Holiday +
+ + + +
+ +
+ + + + + + + + + + + + + + + +
NoTanggalNama HolidayTypeAksi
+ +
+ +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/application/views/employees/leave_requests.php b/application/views/employees/leave_requests.php new file mode 100644 index 0000000..a4a7a94 --- /dev/null +++ b/application/views/employees/leave_requests.php @@ -0,0 +1,418 @@ +
+ +
+ +
+ +
+ +
+ Leave Requests +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
NoFinger IDEmployeeJenis LeaveTanggal MulaiTanggal SelesaiTotalStatusAksi
+ +
+ +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/application/views/employees/payroll.php b/application/views/employees/payroll.php new file mode 100644 index 0000000..48b3c6a --- /dev/null +++ b/application/views/employees/payroll.php @@ -0,0 +1,831 @@ +
+ +
+ +
+ + +
+ +
+

+ Payroll Karyawan +

+ + + Generate dan kelola payroll karyawan + +
+ +
+ + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ No + + Karyawan + + Gaji Pokok + + Pendapatan + + Potongan + + THP + + Status + + Aksi +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/application/views/employees/shift.php b/application/views/employees/shift.php new file mode 100644 index 0000000..6dfc47a --- /dev/null +++ b/application/views/employees/shift.php @@ -0,0 +1,411 @@ +
+ +
+ +
+ +
+ +
+ Master Shift +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
NoKodeNama ShiftJam MasukJam PulangToleransiJam KerjaTypeAksi
+ +
+ +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/application/views/errors/cli/error_404.php b/application/views/errors/cli/error_404.php new file mode 100644 index 0000000..6984b61 --- /dev/null +++ b/application/views/errors/cli/error_404.php @@ -0,0 +1,8 @@ + + +An uncaught Exception was encountered + +Type: +Message: +Filename: getFile(), "\n"; ?> +Line Number: getLine(); ?> + + + +Backtrace: +getTrace() as $error): ?> + + File: + Line: + Function: + + + + diff --git a/application/views/errors/cli/error_general.php b/application/views/errors/cli/error_general.php new file mode 100644 index 0000000..6984b61 --- /dev/null +++ b/application/views/errors/cli/error_general.php @@ -0,0 +1,8 @@ + + +A PHP Error was encountered + +Severity: +Message: +Filename: +Line Number: + + + +Backtrace: + + + File: + Line: + Function: + + + + diff --git a/application/views/errors/cli/index.html b/application/views/errors/cli/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/views/errors/cli/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/errors/html/error_404.php b/application/views/errors/html/error_404.php new file mode 100644 index 0000000..f96c0d1 --- /dev/null +++ b/application/views/errors/html/error_404.php @@ -0,0 +1,124 @@ + + + + + +<?= $heading; ?> + + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + Kembali ke Dashboard + + +
+ + + \ No newline at end of file diff --git a/application/views/errors/html/error_db.php b/application/views/errors/html/error_db.php new file mode 100644 index 0000000..f5a43f6 --- /dev/null +++ b/application/views/errors/html/error_db.php @@ -0,0 +1,64 @@ + + + + +Database Error + + + +
+

+ +
+ + \ No newline at end of file diff --git a/application/views/errors/html/error_exception.php b/application/views/errors/html/error_exception.php new file mode 100644 index 0000000..8784886 --- /dev/null +++ b/application/views/errors/html/error_exception.php @@ -0,0 +1,32 @@ + + +
+ +

An uncaught Exception was encountered

+ +

Type:

+

Message:

+

Filename: getFile(); ?>

+

Line Number: getLine(); ?>

+ + + +

Backtrace:

+ getTrace() as $error): ?> + + + +

+ File:
+ Line:
+ Function: +

+ + + + + + +
\ No newline at end of file diff --git a/application/views/errors/html/error_general.php b/application/views/errors/html/error_general.php new file mode 100644 index 0000000..a2fcac0 --- /dev/null +++ b/application/views/errors/html/error_general.php @@ -0,0 +1,124 @@ + + + + + +403 Forbidden + + + + + + + + +
+ + +
🔒
+ + +
403
+ + +
Akses Ditolak
+ + +
+ Anda tidak memiliki izin untuk mengakses halaman ini.
+ Silakan hubungi administrator jika ini adalah kesalahan. +
+ + + Kembali + + +
+ + + \ No newline at end of file diff --git a/application/views/errors/html/error_php.php b/application/views/errors/html/error_php.php new file mode 100644 index 0000000..b146f9c --- /dev/null +++ b/application/views/errors/html/error_php.php @@ -0,0 +1,33 @@ + + +
+ +

A PHP Error was encountered

+ +

Severity:

+

Message:

+

Filename:

+

Line Number:

+ + + +

Backtrace:

+ + + + +

+ File:
+ Line:
+ Function: +

+ + + + + + + +
\ No newline at end of file diff --git a/application/views/errors/html/index.html b/application/views/errors/html/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/views/errors/html/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/errors/index.html b/application/views/errors/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/views/errors/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/export/export_siswa_pdf.php b/application/views/export/export_siswa_pdf.php new file mode 100644 index 0000000..6ceccc6 --- /dev/null +++ b/application/views/export/export_siswa_pdf.php @@ -0,0 +1,50 @@ + + + + + + + +

Data Santri

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NoNamaNISKelasAlamatNama WaliNo WAEmailTahun MasukKamarGedungJumlah Kumjungan
nama ?>nis ?>nama_kelas ?>alamat ?>nama_wali ?>nomor_whatsapp ?>email ?>tahun_masuk ?>nama_kamar ?>nama_gedung_kamar ?>jumlah_kunjungan ?>
+ + diff --git a/application/views/history/layout.php b/application/views/history/layout.php new file mode 100644 index 0000000..673198a --- /dev/null +++ b/application/views/history/layout.php @@ -0,0 +1,129 @@ +
+
+
+ +
+
History Kunjungan
+ +
+ +
+ + + + + + + + + + + + + + + + + +
NoNamaNISKelasKamarNama WaliAlamatJam MasukPetugas MasukJam KeluarPetugas Keluar
+
+ +
+
+
+ + diff --git a/application/views/index.html b/application/views/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/application/views/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/views/invoices/detail.php b/application/views/invoices/detail.php new file mode 100644 index 0000000..2606a9a --- /dev/null +++ b/application/views/invoices/detail.php @@ -0,0 +1,1164 @@ +
+
+
+ +
+
Detail Invoice
+
+ Kembali + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + +
To AccountNoTanggalItemQtyHargaSubtotalKeteranganAksi
+
+ +
+
+
+ + + + +
+
+
+ +
+
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/application/views/invoices/layout.php b/application/views/invoices/layout.php new file mode 100644 index 0000000..3377311 --- /dev/null +++ b/application/views/invoices/layout.php @@ -0,0 +1,575 @@ +
+
+
+ +
+
+ + + +
+ + + + +
+ +
+ + + + + + + + + + + + + + + + +
NoNo InvoiceTanggalJatuh TempoCustomerTotalTerbayarSisaStatusAksi
+ + +
+
+
+ + + + + + + + + + + diff --git a/application/views/items/draft_items.php b/application/views/items/draft_items.php new file mode 100644 index 0000000..d9b1139 --- /dev/null +++ b/application/views/items/draft_items.php @@ -0,0 +1,757 @@ +
+
+
+ +
+
Data Barang Dalam Perjalanan
+
+ +
+
+ +
+ + + + + + + + + + + + + + + +
NoTanggal BeliKode BarangNamaGudangStokHarga BeliHarga JualCreate atAksi
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/views/items/layout.php b/application/views/items/layout.php new file mode 100644 index 0000000..d4dbe5c --- /dev/null +++ b/application/views/items/layout.php @@ -0,0 +1,685 @@ +
+
+
+ +
+
Data Barang
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + +
NoTanggal BeliKode BarangNamaGudangStokHarga BeliHarga JualCreate atAksi
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/views/jurnal/layout.php b/application/views/jurnal/layout.php new file mode 100644 index 0000000..aaf5e68 --- /dev/null +++ b/application/views/jurnal/layout.php @@ -0,0 +1,510 @@ +
+
+
+ +
+
Jurnal Umum
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
NoTanggalNo RefKeteranganDebitKreditAksi
TOTAL00
+
+ +
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/application/views/kamar/layout.php b/application/views/kamar/layout.php new file mode 100644 index 0000000..a4b079b --- /dev/null +++ b/application/views/kamar/layout.php @@ -0,0 +1,241 @@ +
+
+
+ +
+
Data Kamar
+
+ + + +
+
+ +
+ + + + + + + + + + +
NoNama KamarGedungAksi
+
+ +
+
+
+ + + + + + + + diff --git a/application/views/kelas/layout.php b/application/views/kelas/layout.php new file mode 100644 index 0000000..4504fc9 --- /dev/null +++ b/application/views/kelas/layout.php @@ -0,0 +1,253 @@ +
+
+
+ +
+
Data Kelas
+
+ + + +
+
+ +
+ + + + + + + + + + + +
NoNama KelasGedungWali KelasAksi
+
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/application/views/kode_barang/layout.php b/application/views/kode_barang/layout.php new file mode 100644 index 0000000..351382e --- /dev/null +++ b/application/views/kode_barang/layout.php @@ -0,0 +1,168 @@ + +
+
+
+ +
+
Kode Barang
+ +
+ +
+ + + + + + + + + + +
NoKodeNamaLimit StokAksi
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/application/views/laporan/laba_rugi.php b/application/views/laporan/laba_rugi.php new file mode 100644 index 0000000..271a484 --- /dev/null +++ b/application/views/laporan/laba_rugi.php @@ -0,0 +1,172 @@ +
+
+
+ +

Laporan Laba Rugi

+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/application/views/laporan/neraca.php b/application/views/laporan/neraca.php new file mode 100644 index 0000000..91fa22e --- /dev/null +++ b/application/views/laporan/neraca.php @@ -0,0 +1,151 @@ +
+ + +
+
+ +

NERACA


+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
ACTIVA
+ + + + + + + + + +
Total Activa
+ +
+
+
+ + +
+
+
+ +
PASIVA
+ + + + + + + + + +
Total Pasiva
+ +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/application/views/laporan/neraca_saldo.php b/application/views/laporan/neraca_saldo.php new file mode 100644 index 0000000..ff1898b --- /dev/null +++ b/application/views/laporan/neraca_saldo.php @@ -0,0 +1,66 @@ +
+
+
+ +
+

Neraca Saldo

+
+ +
+ + + + + + + + + + + + + + + + + +
KodeAkunDebitKredit
Total
+
+ +
+
+
+ + \ No newline at end of file diff --git a/application/views/lokasi_asset/layout.php b/application/views/lokasi_asset/layout.php new file mode 100644 index 0000000..ca0d1cc --- /dev/null +++ b/application/views/lokasi_asset/layout.php @@ -0,0 +1,153 @@ +
+
+
+ +
+
Data Lokasi Aset
+ +
+ +
+ + + + + + + + +
NoNama LokasiAksi
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/application/views/partials/footer.php b/application/views/partials/footer.php new file mode 100644 index 0000000..037c839 --- /dev/null +++ b/application/views/partials/footer.php @@ -0,0 +1,261 @@ + + + + + + + + + diff --git a/application/views/partials/header.php b/application/views/partials/header.php new file mode 100644 index 0000000..5be8ece --- /dev/null +++ b/application/views/partials/header.php @@ -0,0 +1,394 @@ +session->userdata('role'); ?> + + + + + + + Accounting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ 🌿 Sekolah Islami + +
+ +
+ + + + + + Logout + + +
+
diff --git a/application/views/pesan/layout.php b/application/views/pesan/layout.php new file mode 100644 index 0000000..33382f9 --- /dev/null +++ b/application/views/pesan/layout.php @@ -0,0 +1,109 @@ + + +
+
+
+ +
+
Riwayat Pesan
+
+ +
+ + + + + + + + + + + + +
NoWaktuTipePenerimaPesanStatus
+
+ +
+
+
+ + + + + + diff --git a/application/views/scan/qr_scan.php b/application/views/scan/qr_scan.php new file mode 100644 index 0000000..d5fb580 --- /dev/null +++ b/application/views/scan/qr_scan.php @@ -0,0 +1,207 @@ +
+
+
+
Silahkan Scan QR
+
+
Hasil scan akan muncul di sini
+ + Kembali +
+
+
+ + + + diff --git a/application/views/setting/layout.php b/application/views/setting/layout.php new file mode 100644 index 0000000..d178eb2 --- /dev/null +++ b/application/views/setting/layout.php @@ -0,0 +1,444 @@ + +
+ +
+
Pengaturan
+
+ +
+ + +
+
+
+ +
Umum
+
+ +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+
+
+ + +
+
+
+ +
Setting Card
+
+
+ + +
+ + Preview akan berubah otomatis +
+ +
+ +
+ + +
+ + + +
+ +
+
+
+ + + +
+
+
+ +
Setting Whatsapp
+
+ +
+ +
+ + +
+ + + + + + + + + +
+ +
+
+
+ + + +
+
+
+ +
Setting Template Pesan
+
+ + + + + + + + + + + + $row): ?> + + + + + + + + + + + + +
NoNamaPesanAksi
nama_template ?>isi_template ?> + +
+ +
+
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/application/views/siswa/detail.php b/application/views/siswa/detail.php new file mode 100644 index 0000000..74480e1 --- /dev/null +++ b/application/views/siswa/detail.php @@ -0,0 +1,462 @@ +foto) ? base_url('uploads/siswa/'.$siswa->foto) : base_url('uploads/siswa/foto.jpg') ; + +?> + + +
+ +
+ +
+ +
+
+

Detail Santri

+
+ +
+ + +
+
+
+ +
Informasi Santri
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nama: nama; ?>
NIS: nis; ?>
Kelas: nama_kelas; ?>
Kamar: nama_kamar . ' ('.$siswa->nama_gedung_kamar.')'; ?>
Tahun Masuk: tahun_masuk; ?>
Alamat: alamat; ?>
Nama Wali Santri: nama_wali; ?>
No Whatsapp Wali Santri: nomor_whatsapp; ?>
Email Wali Santri: email; ?>
Jumlah Kunjungan: jumlah_kunjungan; ?>
+ +
+
+
+ + +
+
+
+ +
Kartu Tanda Wali Santri
+
+ +
+ +
+
+ +
+ + + +
+
+
+ +
+ +
+ +
+
+ +
+
Riwayat Kunjungan
+
+ +
+ + + + + + + + + + + + + + + + + +
NoNamaNISKelasKamarNama WaliAlamatJam MasukPetugas MasukJam KeluarPetugas Keluar
+
+ +
+
+
+
+ + + + + + + + + + + diff --git a/application/views/siswa/layout.php b/application/views/siswa/layout.php new file mode 100644 index 0000000..64c8f1f --- /dev/null +++ b/application/views/siswa/layout.php @@ -0,0 +1,601 @@ + + +
+
+
+ +
+
Data Santri
+ +
+ + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + +
NoFotoNamaNISKelasKamarAlamatNama WaliNomor WhatsappJumlah KunjunganAksi
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/views/users/layout.php b/application/views/users/layout.php new file mode 100644 index 0000000..7a4c851 --- /dev/null +++ b/application/views/users/layout.php @@ -0,0 +1,291 @@ +
+
+
+ +
+
Manajemen Pengguna
+ + +
+ +
+ + + + + + + + + + + +
IDUsernameNamaRoleAksi
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/application/views/warehouses/layout.php b/application/views/warehouses/layout.php new file mode 100644 index 0000000..9dfbab8 --- /dev/null +++ b/application/views/warehouses/layout.php @@ -0,0 +1,153 @@ +
+
+
+ +
+
Data Gudang
+ +
+ +
+ + + + + + + + +
NoNama GudangAksi
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/application/views/welcome_message.php b/application/views/welcome_message.php new file mode 100644 index 0000000..9db22bc --- /dev/null +++ b/application/views/welcome_message.php @@ -0,0 +1,100 @@ + + + + + Welcome to CodeIgniter + + + + + +
+

Welcome to CodeIgniter!

+ +
+

The page you are looking at is being generated dynamically by CodeIgniter.

+ +

If you would like to edit this page you'll find it located at:

+ application/views/welcome_message.php + +

The corresponding controller for this page is found at:

+ application/controllers/Welcome.php + +

If you are exploring CodeIgniter for the very first time, you should start by reading the User Guide.

+
+ + +
+ + + diff --git a/assets/css/style-custom.css b/assets/css/style-custom.css new file mode 100644 index 0000000..1524d26 --- /dev/null +++ b/assets/css/style-custom.css @@ -0,0 +1,1171 @@ +:root { + --green-light: #ffb347; + --green-main: #ff8c00; + --green-dark: #cc7000; + --green-emerald: #ff9933; + --gold-soft: #e8cfa3; + --bg-premium: #fff8f2; + --border-premium: rgba(255,140,0,0.12); +} + +/* Global */ +html, body { + background: var(--bg-premium); + font-family: "Poppins", sans-serif; + color: #1d2d2d; + scroll-behavior: smooth; + font-size: 13px; +} + +/* ───────────────────── NAVBAR PREMIUM SUPER MODERN ───────────────────── */ +.navbar { + background: linear-gradient(180deg, #ff8c00, #ff8c00); + -webkit-backdrop-filter: blur(6px); + border-bottom: 1px solid rgba(255, 255, 255, 0.18); + padding: 0.65rem 0; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); + transition: 0.3s ease; +} + +/* BRAND */ +.brand-title { + font-weight: 600; + font-size: 1.15rem; + letter-spacing: 0.5px; + color: #ffffff !important; +} + +/* NAVLINK STYLE */ +.navbar .nav-link { + color: rgba(255,255,255,0.92) !important; + font-weight: 500; + padding: 0.65rem 1rem; + border-radius: 12px; + transition: 0.3s ease; + position: relative; + display: flex; + align-items: center; + gap: 8px; + margin-left: 10px; +} + +/* Hover glow */ +.navbar .nav-link:hover { + background: rgba(255,255,255,0.22); + color: #fff !important; + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(255,255,255,0.25); +} + +/* Active */ +.navbar .nav-link.active { + background: #fff3e0; + color: var(--green-main) !important; + font-weight: 600; + box-shadow: 0 4px 14px rgba(0,0,0,0.15); +} + +/* Underline animation */ +.navbar .nav-link::after { + content: ""; + width: 0; + height: 2.5px; + background: #ffffffb7; + border-radius: 10px; + position: absolute; + bottom: 6px; + left: 50%; + transition: 0.3s ease; + transform: translateX(-50%); +} + +.navbar .nav-link:hover::after { width: 70%; } +.navbar .nav-link.active::after { + width: 80%; + background: var(--green-main); +} + +/* ───────────────────── DROPDOWN - ANIMASI PREMIUM ───────────────────── */ + +/* Dropdown base */ +.navbar .dropdown-menu { + opacity: 0; + visibility: hidden; + transform: translateY(14px); + transition: 0.25s ease; + border-radius: 14px; + padding: 0.4rem; + background: #ffffff; + box-shadow: 0 14px 30px rgba(0,0,0,0.1); + border: 1px solid rgba(0,0,0,0.05); +} + +/* Dropdown item */ +.navbar .dropdown-item { + border-radius: 10px; + padding: 0.55rem 1rem; + transition: 0.25s ease; + margin-bottom: 2px; +} + +.navbar .dropdown-item:hover { + background: rgba(255,140,0,0.18); + color: var(--green-main); +} + +.navbar .dropdown-item.active { + background: rgba(255,140,0,0.18); + color: var(--green-main); +} + +/* ──────────────── SHOW DROPDOWN ON HOVER (DESKTOP ONLY) ─────────────── */ +@media (min-width: 992px) { + .navbar .dropdown:hover .dropdown-menu { + display: block; + opacity: 1; + visibility: visible; + transform: translateY(6px); + } +} + +/* MOBILE FIX: dropdown tetap klik, bukan hover */ +@media (max-width: 991px) { + .navbar .dropdown-menu { + opacity: 1 !important; + visibility: visible !important; + transform: none !important; + } +} + +/* ───────────────────── BUTTON PREMIUM (User Dropdown) ───────────────────── */ +.btn-premium { + background: linear-gradient(180deg, #ff9933, #ff8c00); + color: #fff; + padding: 0.55rem 1.1rem; + border-radius: 50px; + border: 1px solid rgba(255,255,255,0.20); + font-weight: 500; + box-shadow: 0 4px 14px rgba(255,140,0,0.25); + transition: all 0.3s ease; +} + +.btn-premium:hover { + background: linear-gradient(180deg, #ffb347, #ff8c00); + transform: translateY(-1px); + box-shadow: 0 7px 18px rgba(255,140,0,0.35); +} + + +/* ─────────────────────────────────────────── + SIDEBAR + SUBMENU (OFFCANVAS) PREMIUM +─────────────────────────────────────────── */ + +/* Sidebar container */ +.offcanvas { + background: #ffffff; + border-right: 1px solid rgba(0,0,0,0.05); + padding-top: 1.2rem; + border-radius: 0 16px 16px 0; +} + +/* Logo */ +.offcanvas-header .brand-mobile { + font-size: 1.3rem; + font-weight: 700; + color: var(--green-main); +} + +/* Menu list */ +.offcanvas .nav { + padding: 0 1rem; +} + +/* Menu item */ +.offcanvas .nav-link { + font-weight: 600; + border-radius: 14px; + padding: 0.78rem 1rem; + margin-bottom: 4px; + display: flex; + justify-content: space-between; + align-items: center; + color: #444; + transition: 0.25s ease; +} + +/* Left text + icon */ +.offcanvas .nav-link .menu-wrap { + display: flex; + align-items: center; + gap: 12px; +} + +/* Icon style */ +.offcanvas .nav-link i { + font-size: 1.1rem; + color: var(--green-main); + opacity: 0.8; + transition: 0.25s; +} + +/* Hover */ +.offcanvas .nav-link:hover { + background: rgba(255,140,0,0.12); + color: var(--green-main); + transform: translateX(4px); +} + +/* Active */ +.offcanvas .nav-link.active { + background: rgba(255,140,0,0.18); + color: var(--green-main); + font-weight: 700; +} + +/* Dropdown arrow */ +.menu-arrow { + transition: 0.25s; + font-size: 0.95rem; + opacity: 0.7; +} + +/* Rotate arrow when expanded */ +.nav-link[aria-expanded="true"] .menu-arrow { + transform: rotate(90deg); + opacity: 1; +} + +/* SUBMENU container */ +.submenu { + padding-left: 1.7rem; + margin-bottom: 6px; +} + +/* Submenu link */ +.submenu .sub-link { + padding: 0.6rem 0.9rem; + border-radius: 12px; + display: block; + color: #555; + font-weight: 500; + transition: 0.25s ease; +} + +.submenu .sub-link:hover { + background: rgba(255,140,0,0.12); + color: var(--green-main); +} + +/* Active submenu */ +.submenu .sub-link.active { + background: rgba(255,140,0,0.18); + color: var(--green-main); + font-weight: 600; +} + +/* Divider */ +.sidebar-divider { + border-bottom: 1px solid rgba(0,0,0,0.06); + margin: 1rem 0; +} + +/* Logout button */ +.btn-logout { + margin-top: 10px; + width: 100%; + padding: 0.75rem; + border-radius: 14px; + background: rgba(220,53,69,0.12); + color: #dc3545; + font-weight: 600; + transition: 0.25s ease; +} + +.btn-logout:hover { + background: rgba(220,53,69,0.18); + color: #c82333; +} + + + +/* ───────────────────── SUMMARY CARDS ───────────────────── */ +.summary-card { + background: #ffffff; + border: 1px solid var(--border-premium); + border-radius: 16px; + padding: 1.35rem; + transition: 0.28s ease; + box-shadow: 0 6px 14px rgba(0,0,0,0.03); +} + +.summary-card:hover { + transform: translateY(-5px); + box-shadow: 0 14px 28px rgba(0,0,0,0.08); + border-color: var(--green-main); +} + +.summary-icon { + width: 60px; + height: 60px; + display: grid; + place-items: center; + border-radius: 14px; + background: linear-gradient(180deg, rgba(255,140,0,0.10), rgba(255,140,0,0.05)); + font-size: 1.6rem; + color: var(--green-main); +} + +/* ───────────────────── CHART PLACEHOLDER ───────────────────── */ +.chart-placeholder { + height: 340px; + border-radius: 16px; + background: linear-gradient(180deg, #ffffff, #fff8f2); + border: 1px dashed rgba(255,140,0,0.18); + display: flex; + justify-content: center; + align-items: center; + color: #889; + font-size: 1.05rem; +} + +/* ─────────────────────────────── + BASE WRAPPER (Responsive) +────────────────────────────────*/ +.table-responsive-premium { + width: 100%; + overflow-x: auto; + scrollbar-width: thin; + padding-bottom: 8px; +} + +.table-responsive-premium::-webkit-scrollbar { + height: 6px; +} + +.table-responsive-premium::-webkit-scrollbar-thumb { + background: #ffd8a8; + border-radius: 10px; +} + +/* ─────────────────────────────── + CARD WRAPPER +────────────────────────────────*/ +.table-card { + background: #ffffff; + border-radius: 18px; + padding: 1.2rem; + box-shadow: 0 8px 22px rgba(0,0,0,0.05); + border: 1px solid rgba(0,0,0,0.05); + transition: 0.3s ease; +} + +.table-card:hover { + box-shadow: 0 12px 28px rgba(0,0,0,0.08); +} + +/* ─────────────────────────────── + MODERN TABLE +────────────────────────────────*/ +.modern-table { + width: 100%; + border-collapse: separate; + border-spacing: 0 12px; +} + +/* HEADER */ +.modern-table thead tr { + background: linear-gradient(90deg, #ff8c00, #ffb347); +} + +.modern-table thead th { + color: #fff; + padding: 14px; + text-transform: uppercase; + font-size: 0.78rem; + border: none !important; + letter-spacing: 0.5px; +} + +/* BODY */ +.modern-table tbody tr { + background: #ffffff; + border-radius: 16px; + box-shadow: 0 5px 14px rgba(0,0,0,0.06); + transition: 0.25s ease; +} + +.modern-table tbody tr td:first-child { + border-top-left-radius: 16px; + border-bottom-left-radius: 16px; +} + +.modern-table tbody tr td:last-child { + border-top-right-radius: 16px; + border-bottom-right-radius: 16px; +} + +.modern-table td { + padding: 14px; + vertical-align: middle; + border: none; + font-size: 0.95rem; +} + +/* HOVER EFFECT */ +.modern-table tbody tr:hover { + transform: translateY(-3px); + box-shadow: 0 10px 28px rgba(0,0,0,0.10); + background: #fff3e0; +} + +/* ─────────────────────────────── + BUTTONS +────────────────────────────────*/ +.btn-action { + border: none; + padding: 6px 14px; + border-radius: 10px; + font-weight: 500; + transition: 0.25s; + font-size: 0.85rem; + margin: 2px; +} + +.btn-edit { + background: #ffcc85; + color: #6a3c00; +} + +.btn-edit:hover { + background: #ffb963; +} + +.btn-delete { + background: #ff6b6b; + color: #fff; +} + +.btn-delete:hover { + background: #ff3a3a; +} + +.btn-add, +.btn-eximport { + background: #ff8c00; + color: #fff; + padding: 8px 20px; + border-radius: 50px; + font-weight: 600; + box-shadow: 0 4px 14px rgba(0,0,0,0.15); + transition: 0.25s ease; +} + +.btn-add:hover { + transform: translateY(-2px); + background: #ff9933; +} + +.btn-eximport:hover { + transform: translateY(-2px); + background: #ff9933; +} + +/* ─────────────────────────────── + RESPONSIVE TWEAK +────────────────────────────────*/ +@media (max-width: 768px) { + .modern-table thead { + display: none; + } + + .modern-table tbody tr { + display: block; + margin-bottom: 14px; + } + + .modern-table td { + display: flex; + justify-content: space-between; + padding: 12px; + font-size: 0.9rem; + } + + .modern-table td::before { + content: attr(data-label); + font-weight: 600; + color: #ff8c00; + } +} + +/* ====================================================== + CARD WRAPPER — Clean & Modern +====================================================== */ +.modern-card { + background: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0,0,0,0.08); + transition: 0.25s ease; + border: none; +} + +/* ====================================================== + BUTTON TAMBAH — Minimalis +====================================================== */ + +.btn-add, +.btn-eximport { + background-color: #ff8c00; + color: #fff; + border-radius: 8px; + padding: 6px 14px; + font-weight: 500; + font-size: 0.875rem; + transition: 0.2s; +} +.btn-add:hover { + background-color: #ff9933; +} +.btn-eximport:hover { + background-color: #ff9933; +} + +/* ====================================================== + TABLE HEADER — Modern & Elegan (Standar + Rounded) +====================================================== */ +table.modern-table thead { + background-color: #ff8c00; +} + +table.modern-table thead tr { + color: #ffffff !important; + font-weight: 600; + letter-spacing: 0.5px; +} + +table.modern-table thead th { + padding: 12px 15px; + font-size: 0.8rem; + text-transform: uppercase; + border-bottom: 2px solid rgba(255,255,255,0.2); + background-color: #ff8c00 !important; + color: #ffffff !important; +} + +/* Border radius untuk ujung kiri & kanan */ +table.modern-table thead th:first-child { + border-top-left-radius: 8px; +} + +table.modern-table thead th:last-child { + border-top-right-radius: 8px; +} + +/* ====================================================== + TABLE BODY — Modern Clean +====================================================== */ +table.modern-table tbody tr { + background-color: #ffffff; + border-radius: 8px; + transition: 0.2s; +} +table.modern-table tbody tr:hover { + background-color: #fff3e0; +} +table.modern-table tbody td { + padding: 10px; + border-top: 1px solid #efefef; + vertical-align: middle; +} + +/* Rounded corners for first & last td */ +table.modern-table tbody tr td:first-child { + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; +} +table.modern-table tbody tr td:last-child { + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; +} + +/* ====================================================== + ACTION BUTTONS — Modern & Subtle +====================================================== */ +.btn-action { + border: none; + padding: 5px 10px; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 500; + transition: 0.2s; + margin: 2px; +} + +.btn-edit { + background-color: #fbcf82; + color: #5a3600; +} +.btn-edit:hover { + background-color: #f9b75b; +} + +.btn-delete { + background-color: #ff6b6b; + color: #fff; +} +.btn-delete:hover { + background-color: #e03e3e; +} + +.btn-detail { + background-color: #4dabf7; + color: #fff; +} +.btn-detail:hover { + background-color: #339af0; +} + +/* ====================================================== + DATA TABLE TOP CONTROLS — Paksa Kiri/Kanan selalu +====================================================== */ +.dataTables_wrapper .dataTables_top { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + flex-wrap: nowrap !important; + gap: 5px; + margin-bottom: 12px; + box-sizing: border-box; + height: auto; +} + +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_filter { + display: flex !important; + align-items: center !important; + gap: 5px; + margin: 0; + font-size: 0.9rem; + flex: 0 0 auto; +} + +.dataTables_wrapper .dataTables_length label, +.dataTables_wrapper .dataTables_filter label { + display: flex !important; + align-items: center !important; + font-size: 0; + margin: 0; + padding: 0; +} + +/* Styling select & input */ +.dataTables_wrapper .dataTables_length select{ + padding: 5px 10px; + border-radius: 6px; + border: 1px solid #ced4da; + font-size: 0.85rem; + outline: none; + transition: 0.2s; + margin: 0; + box-sizing: border-box; +} + +.dataTables_wrapper .dataTables_filter input { + padding: 5px 10px; + border-radius: 6px; + border: 1px solid #ced4da; + font-size: 0.85rem; + outline: none; + transition: 0.2s; + margin: -30px 0 0 0; + box-sizing: border-box; + min-width: 300px; + /*placeholder: "Search";*/ +} + +/* Dorong search ke kanan */ +.dataTables_wrapper .dataTables_filter { + justify-content: flex-end !important; +} + +/* Fokus styling */ +.dataTables_wrapper .dataTables_length select:focus, +.dataTables_wrapper .dataTables_filter input:focus { + border-color: #ff8c00; + box-shadow: 0 0 5px rgba(255,140,0,0.3); +} + +/* ====================================================== + Responsive HP — tetap kiri-kanan, tidak stack +====================================================== */ +@media (max-width: 1020px) { + .dataTables_wrapper .dataTables_top { + flex-direction: row !important; + align-items: center !important; + justify-content: space-between !important; + flex-wrap: nowrap !important; + } + + .dataTables_wrapper .dataTables_length, + .dataTables_wrapper .dataTables_filter { + flex: 0 0 auto !important; + width: auto !important; + margin-bottom: 0 !important; + } +} + +/* ====================================================== + DATA TABLE FOOTER (INFO + PAGINATION) — Centered +====================================================== */ +.dataTables_wrapper .dataTables_bottom { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: 12px; + gap: 6px; +} + +.dataTables_wrapper .dataTables_info { + font-size: 0.85rem; + color: #6c757d; + width: 100%; + text-align: center; + float: none !important; + margin: 0 !important; +} + +.dataTables_wrapper .dataTables_paginate { + width: 100%; + text-align: center; + float: none !important; + margin: 0 !important; + display: flex !important; + justify-content: center !important; + gap: 4px; +} + +/* Pagination buttons */ +.dataTables_wrapper .dataTables_paginate .paginate_button { + display: inline-block; + background: #ffffff; + border: 1px solid #ced4da; + color: #ff8c00; + border-radius: 6px; + padding: 3px 8px; + margin: 2px; + transition: 0.2s; + cursor: pointer; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button.current, +.dataTables_wrapper .dataTables_paginate .paginate_button:hover { + background: #ff8c00; + color: #fff !important; + border: 1px solid #ff8c00; +} + +/* Remove extra borders from table */ +table.modern-table, +table.modern-table th, +table.modern-table td { + border: none; +} + +/* Responsive fix tambahan untuk HP */ +@media (max-width: 768px) { + .dataTables_wrapper .dataTables_bottom { + flex-direction: column; + align-items: center; + gap: 6px; + } + .dataTables_wrapper .dataTables_info, + .dataTables_wrapper .dataTables_paginate { + width: 100%; + text-align: center; + float: none !important; + margin: 0 !important; + } + .dataTables_wrapper .dataTables_paginate { + justify-content: center !important; + display: flex !important; + } +} + +/* ====================================================== + RESPONSIVE MOBILE FIX +====================================================== */ +@media (max-width: 768px) { + table.modern-table thead { display: none; } + + table.modern-table tbody tr { + display: block; + margin-bottom: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + border-radius: 8px; + padding: 10px; + } + + table.modern-table tbody td { + display: flex; + justify-content: space-between; + padding: 8px 10px; + font-size: 0.875rem; + border-bottom: 1px solid #efefef; + } + + table.modern-table tbody td:last-child { border-bottom: none; } + + table.modern-table tbody td::before { + content: attr(data-label); + font-weight: 600; + color: #ff8c00; + margin-right: 8px; + flex: 1; + } + + table.modern-table tbody td span { + flex: 2; + text-align: right; + } + + .dataTables_wrapper .dataTables_top, + .dataTables_wrapper .dataTables_bottom { + flex-direction: column; + align-items: center; + gap: 10px; + } + + .dataTables_wrapper .dataTables_length, + .dataTables_wrapper .dataTables_filter, + .dataTables_wrapper .dataTables_info, + .dataTables_wrapper .dataTables_paginate { + margin-bottom: 6px; + } +} + + +/* ============================================================ + ★★★ GLOBAL CARD STYLE (POTRET & LANDSCAPE) ★★★ +============================================================ */ +.id-card-potret, +.id-card-lemscape { + background: var(--bg) center/cover no-repeat; + border-radius: 18px; + overflow: hidden; + padding: 12px; + display: flex; + justify-content: center; + align-items: flex-start; + box-shadow: 0 10px 22px rgba(0,0,0,0.15); + border: 1px solid rgba(255,255,255,0.25); +} + +/* Ukuran */ +.id-card-potret { + width: 300px; + height: 480px; +} +.id-card-lemscape { + width: 480px; + height: 300px; +} + +/* Inner */ +.card-inner { + width: 100%; + height: 100%; + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; +} + +/* ============================================================ + POTRET +============================================================ */ +.id-card-potret .card-inner { + justify-content: flex-start; +} + +/* Logo */ +.card-logo { + width: 230px; + margin-bottom: 8px; +} +.card-logo img { + width: 100%; + object-fit: contain; +} + +/* Title */ +.card-title { + font-size: 14px; + font-weight: 700; + text-decoration: underline; + margin-bottom: 12px; + text-align: center; +} + +/* Foto */ +.id-card-potret .photo-wrapper { + width: 90px; + height: 170px; + border-radius: 14px; + overflow: hidden; + margin-bottom: 12px; + border: 1px solid rgba(255,255,255,0.4); + box-shadow: 0 6px 16px rgba(0,0,0,0.25); + background: #fff; +} +.id-card-potret .photo-wrapper img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Info */ +.id-card-potret .card-info { + text-align: center; + margin-bottom: 14px; +} +.card-info .nama { + font-size: 15px; + font-weight: 700; +} +.card-info .nis, +.card-info .kelas, +.card-info .alamat { + font-size: 12px; + margin-top: 2px; +} + +/* QR */ +.id-card-potret .qr-wrapper { + width: 140px; + height: 140px; +} +.qr-wrapper img { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* ============================================================ + LANDSCAPE +============================================================ */ +.id-card-lemscape .card-inner { + align-items: center; +} + +/* Logo */ +.id-card-lemscape .card-logo { + width: 230px; + margin-bottom: 6px; +} + +/* Title */ +.id-card-lemscape .card-title { + width: 100%; + text-align: center; + margin-bottom: 8px; +} + +/* Content */ +.landscape-content { + width: 100%; + display: flex; + align-items: center; + margin-top: 12px; +} + +/* Foto kiri */ +.id-card-lemscape .photo-wrapper { + width: 80px; + height: 100px; + border-radius: 14px; + overflow: hidden; + border: 1px solid rgba(255,255,255,0.4); + box-shadow: 0 6px 18px rgba(0,0,0,0.25); + margin-right: 10px; + background: #fff; +} +.id-card-lemscape .photo-wrapper img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Info */ +.id-card-lemscape .card-info { + margin: 0 10px; + font-size: 12px; +} + +/* QR kanan */ +.id-card-lemscape .qr-wrapper { + width: 140px; + height: 140px; + margin-left: auto; +} + +/* Center helper */ +.items-card-center { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +/* ============================================================ + FOTO SANTRI +============================================================ */ +.card-foto-santri { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.foto-santri { + width: 160px; + height: 160px; + background: #f3f5f9; + border-radius: 50%; + border: 2px solid #ced4da; + box-shadow: 4px 4px 12px rgba(0,0,0,0.08), -4px -4px 12px rgba(255,255,255,0.9); + background-size: cover; + background-position: center; + position: relative; + transition: 0.3s; + cursor: pointer; +} + +.foto-santri:hover { + transform: scale(1.05); +} + +/* icon kamera */ +.foto-santri::after { + content: '📷'; + position: absolute; + bottom: 0; + right: 0; + font-size: 22px; + background: rgba(255,255,255,0.85); + border-radius: 50%; + padding: 4px; +} + +/* input */ +.card-foto-santri input[type="file"] { + width: 100%; + padding: 6px 10px; + border-radius: 10px; + border: 1px solid #ced4da; +} + +/* list kecil */ +.foto-santri-list { + width: 45px; + height: 45px; + border-radius: 50%; + background-size: cover; + background-position: center; + border: 2px solid #ced4da; +} + +/* ============================================================ + SCAN MENU +============================================================ */ +.scan-menu { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-top: 20px; +} + +.scan-card { + background: linear-gradient(180deg, #db9a00, #c48700); + border-radius: 18px; + padding: 22px 10px; + text-align: center; + color: #fff; + box-shadow: 0 6px 18px rgba(0,0,0,0.18); + transition: 0.25s; + cursor: pointer; + position: relative; + overflow: hidden; +} + +.scan-card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 28px rgba(0,0,0,0.25); +} + +/* efek glow */ +.scan-card::before { + content: ""; + position: absolute; + top: -70px; + left: -70px; + width: 150px; + height: 150px; + background: rgba(255,255,255,0.15); + border-radius: 50%; + transition: 0.4s; +} +.scan-card:hover::before { + top: -20px; + left: -20px; +} + +.scan-card img { + width: 80px; + margin-bottom: 10px; +} + +.scan-card h6 { + font-size: 14px; + font-weight: 600; +} + +/* ============================================================ + BADGE TABLE +============================================================ */ +table.dataTable tbody tr td span.badge { + display: inline-flex !important; + align-items: center; + white-space: nowrap; + width: max-content; + padding: 3px 6px; + font-size: 10px; + border-radius: 6px; +} + +/* ============================================================ + DATERANGEPICKER +============================================================ */ +.daterangepicker .applyBtn { + background: var(--green-main); +} +.daterangepicker td.active { + background: var(--green-main); +} + +/* single calendar */ +.daterangepicker .drp-calendar.right { + display: none !important; +} +.daterangepicker .cancelBtn { + display: none !important; +} + +.bg-warning { + background: var(--green-main) !important; +} + +.btn-warning { + background: var(--green-main) !important; + color: #ffffff !important; +} \ No newline at end of file diff --git a/assets/img/accounting-color.png b/assets/img/accounting-color.png new file mode 100644 index 0000000..c9ea69d Binary files /dev/null and b/assets/img/accounting-color.png differ diff --git a/assets/img/accounting-light.png b/assets/img/accounting-light.png new file mode 100644 index 0000000..942e3bd Binary files /dev/null and b/assets/img/accounting-light.png differ diff --git a/assets/img/accounting-white.png b/assets/img/accounting-white.png new file mode 100644 index 0000000..cf1bcd1 Binary files /dev/null and b/assets/img/accounting-white.png differ diff --git a/assets/img/accounting.png b/assets/img/accounting.png new file mode 100644 index 0000000..c24b80f Binary files /dev/null and b/assets/img/accounting.png differ diff --git a/assets/img/bg-card.jpg b/assets/img/bg-card.jpg new file mode 100644 index 0000000..5c337cb Binary files /dev/null and b/assets/img/bg-card.jpg differ diff --git a/assets/img/bg-lemscape.jpg b/assets/img/bg-lemscape.jpg new file mode 100644 index 0000000..5a1a0f9 Binary files /dev/null and b/assets/img/bg-lemscape.jpg differ diff --git a/assets/img/bg-potret.jpg b/assets/img/bg-potret.jpg new file mode 100644 index 0000000..22eb8ee Binary files /dev/null and b/assets/img/bg-potret.jpg differ diff --git a/assets/img/foto.jpg b/assets/img/foto.jpg new file mode 100644 index 0000000..75dede6 Binary files /dev/null and b/assets/img/foto.jpg differ diff --git a/assets/img/logo-sm.png b/assets/img/logo-sm.png new file mode 100644 index 0000000..fc38d37 Binary files /dev/null and b/assets/img/logo-sm.png differ diff --git a/assets/img/logo.png b/assets/img/logo.png new file mode 100644 index 0000000..00bec61 Binary files /dev/null and b/assets/img/logo.png differ diff --git a/assets/img/qr.png b/assets/img/qr.png new file mode 100644 index 0000000..cfda1b5 Binary files /dev/null and b/assets/img/qr.png differ diff --git a/assets/img/scan-keluar.jpg b/assets/img/scan-keluar.jpg new file mode 100644 index 0000000..7510d16 Binary files /dev/null and b/assets/img/scan-keluar.jpg differ diff --git a/assets/img/scan-masuk.jpg b/assets/img/scan-masuk.jpg new file mode 100644 index 0000000..771a75e Binary files /dev/null and b/assets/img/scan-masuk.jpg differ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7b13fe0 --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "description": "The CodeIgniter framework", + "name": "codeigniter/framework", + "type": "project", + "homepage": "https://codeigniter.com", + "license": "MIT", + "support": { + "forum": "http://forum.codeigniter.com/", + "wiki": "https://github.com/bcit-ci/CodeIgniter/wiki", + "slack": "https://codeigniterchat.slack.com", + "source": "https://github.com/bcit-ci/CodeIgniter" + }, + "require": { + "php": ">=5.3.7", + "phpoffice/phpspreadsheet": "*", + "dompdf/dompdf": "*" + }, + "suggest": { + "paragonie/random_compat": "Provides better randomness in PHP 5.x" + }, + "scripts": { + "test:coverage": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --color=always --coverage-text --configuration tests/travis/sqlite.phpunit.xml" + ], + "post-install-cmd": [ + "sed -i s/name{0}/name[0]/ vendor/mikey179/vfsstream/src/main/php/org/bovigo/vfs/vfsStream.php" + ], + "post-update-cmd": [ + "sed -i s/name{0}/name[0]/ vendor/mikey179/vfsstream/src/main/php/org/bovigo/vfs/vfsStream.php" + ] + }, + "require-dev": { + "mikey179/vfsstream": "1.6.*", + "phpunit/phpunit": "4.* || 5.* || 9.*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..72b9def --- /dev/null +++ b/composer.lock @@ -0,0 +1,2949 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "29c9e0d6e2ffa327c7e0f8946286419d", + "packages": [ + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "dompdf/dompdf", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.4" + }, + "time": "2025-10-29T12:43:30+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + }, + "time": "2024-12-02T14:37:59+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0" + }, + "time": "2024-04-29T13:26:35+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "2.2.6", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f", + "reference": "30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.5", + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.9", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.6" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + }, + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2022-11-25T18:57:19+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "https://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2025-01-14T11:49:03+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1" + }, + "time": "2025-10-26T16:01:04+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "sabberworm/php-css-parser", + "version": "v8.9.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", + "rawr/cross-data-providers": "^2.0.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + }, + "time": "2025-07-11T13:20:48+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.12", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5||^8.5||^9.6", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2024-08-29T18:43:31+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-12-06T07:45:52+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:51:50+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.7" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..7a20192 --- /dev/null +++ b/index.php @@ -0,0 +1,324 @@ +=')) + { + error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); + } + else + { + error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE); + } + break; + + default: + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'The application environment is not set correctly.'; + exit(1); // EXIT_ERROR +} + +/* + *--------------------------------------------------------------- + * SYSTEM DIRECTORY NAME + *--------------------------------------------------------------- + * + * This variable must contain the name of your "system" directory. + * Set the path if it is not in the same directory as this file. + */ + $system_path = 'system'; + +/* + *--------------------------------------------------------------- + * APPLICATION DIRECTORY NAME + *--------------------------------------------------------------- + * + * If you want this front controller to use a different "application" + * directory than the default one you can set its name here. The directory + * can also be renamed or relocated anywhere on your server. If you do, + * use an absolute (full) server path. + * For more info please see the user guide: + * + * https://codeigniter.com/userguide3/general/managing_apps.html + * + * NO TRAILING SLASH! + */ + $application_folder = 'application'; + +/* + *--------------------------------------------------------------- + * VIEW DIRECTORY NAME + *--------------------------------------------------------------- + * + * If you want to move the view directory out of the application + * directory, set the path to it here. The directory can be renamed + * and relocated anywhere on your server. If blank, it will default + * to the standard location inside your application directory. + * If you do move this, use an absolute (full) server path. + * + * NO TRAILING SLASH! + */ + $view_folder = ''; + + +/* + * -------------------------------------------------------------------- + * DEFAULT CONTROLLER + * -------------------------------------------------------------------- + * + * Normally you will set your default controller in the routes.php file. + * You can, however, force a custom routing by hard-coding a + * specific controller class/function here. For most applications, you + * WILL NOT set your routing here, but it's an option for those + * special instances where you might want to override the standard + * routing in a specific front controller that shares a common CI installation. + * + * IMPORTANT: If you set the routing here, NO OTHER controller will be + * callable. In essence, this preference limits your application to ONE + * specific controller. Leave the function name blank if you need + * to call functions dynamically via the URI. + * + * Un-comment the $routing array below to use this feature + */ + // The directory name, relative to the "controllers" directory. Leave blank + // if your controller is not in a sub-directory within the "controllers" one + // $routing['directory'] = ''; + + // The controller class file name. Example: mycontroller + // $routing['controller'] = ''; + + // The controller function you wish to be called. + // $routing['function'] = ''; + + +/* + * ------------------------------------------------------------------- + * CUSTOM CONFIG VALUES + * ------------------------------------------------------------------- + * + * The $assign_to_config array below will be passed dynamically to the + * config class when initialized. This allows you to set custom config + * items or override any default config values found in the config.php file. + * This can be handy as it permits you to share one application between + * multiple front controller files, with each file containing different + * config values. + * + * Un-comment the $assign_to_config array below to use this feature + */ + // $assign_to_config['name_of_config_item'] = 'value of config item'; + + + +// -------------------------------------------------------------------- +// END OF USER CONFIGURABLE SETTINGS. DO NOT EDIT BELOW THIS LINE +// -------------------------------------------------------------------- + +/* + * --------------------------------------------------------------- + * Resolve the system path for increased reliability + * --------------------------------------------------------------- + */ + + // Set the current directory correctly for CLI requests + if (defined('STDIN')) + { + chdir(dirname(__FILE__)); + } + + if (($_temp = realpath($system_path)) !== FALSE) + { + $system_path = $_temp.DIRECTORY_SEPARATOR; + } + else + { + // Ensure there's a trailing slash + $system_path = strtr( + rtrim($system_path, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ).DIRECTORY_SEPARATOR; + } + + // Is the system path correct? + if ( ! is_dir($system_path)) + { + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'Your system folder path does not appear to be set correctly. Please open the following file and correct this: '.pathinfo(__FILE__, PATHINFO_BASENAME); + exit(3); // EXIT_CONFIG + } + +/* + * ------------------------------------------------------------------- + * Now that we know the path, set the main path constants + * ------------------------------------------------------------------- + */ + // The name of THIS file + define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME)); + + // Path to the system directory + define('BASEPATH', $system_path); + + // Path to the front controller (this file) directory + define('FCPATH', dirname(__FILE__).DIRECTORY_SEPARATOR); + + // Name of the "system" directory + define('SYSDIR', basename(BASEPATH)); + + // The path to the "application" directory + if (is_dir($application_folder)) + { + if (($_temp = realpath($application_folder)) !== FALSE) + { + $application_folder = $_temp; + } + else + { + $application_folder = strtr( + rtrim($application_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + } + elseif (is_dir(BASEPATH.$application_folder.DIRECTORY_SEPARATOR)) + { + $application_folder = BASEPATH.strtr( + trim($application_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + else + { + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'Your application folder path does not appear to be set correctly. Please open the following file and correct this: '.SELF; + exit(3); // EXIT_CONFIG + } + + define('APPPATH', $application_folder.DIRECTORY_SEPARATOR); + + // The path to the "views" directory + if ( ! isset($view_folder[0]) && is_dir(APPPATH.'views'.DIRECTORY_SEPARATOR)) + { + $view_folder = APPPATH.'views'; + } + elseif (is_dir($view_folder)) + { + if (($_temp = realpath($view_folder)) !== FALSE) + { + $view_folder = $_temp; + } + else + { + $view_folder = strtr( + rtrim($view_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + } + elseif (is_dir(APPPATH.$view_folder.DIRECTORY_SEPARATOR)) + { + $view_folder = APPPATH.strtr( + trim($view_folder, '/\\'), + '/\\', + DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR + ); + } + else + { + header('HTTP/1.1 503 Service Unavailable.', TRUE, 503); + echo 'Your view folder path does not appear to be set correctly. Please open the following file and correct this: '.SELF; + exit(3); // EXIT_CONFIG + } + + define('VIEWPATH', $view_folder.DIRECTORY_SEPARATOR); + +/* + * -------------------------------------------------------------------- + * LOAD THE BOOTSTRAP FILE + * -------------------------------------------------------------------- + * + * And away we go... + */ + +date_default_timezone_set('Asia/Jakarta'); + +require_once BASEPATH.'core/CodeIgniter.php'; diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..82be622 --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 - 2022, CodeIgniter Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..63a55c3 --- /dev/null +++ b/readme.rst @@ -0,0 +1,71 @@ +################### +What is CodeIgniter +################### + +CodeIgniter is an Application Development Framework - a toolkit - for people +who build web sites using PHP. Its goal is to enable you to develop projects +much faster than you could if you were writing code from scratch, by providing +a rich set of libraries for commonly needed tasks, as well as a simple +interface and logical structure to access these libraries. CodeIgniter lets +you creatively focus on your project by minimizing the amount of code needed +for a given task. + +******************* +Release Information +******************* + +This repo contains in-development code for future releases. To download the +latest stable release please visit the `CodeIgniter Downloads +`_ page. + +************************** +Changelog and New Features +************************** + +You can find a list of all changes for each release in the `user +guide change log `_. + +******************* +Server Requirements +******************* + +PHP version 5.6 or newer is recommended. + +It should work on 5.3.7 as well, but we strongly advise you NOT to run +such old versions of PHP, because of potential security and performance +issues, as well as missing features. + +************ +Installation +************ + +Please see the `installation section `_ +of the CodeIgniter User Guide. + +******* +License +******* + +Please see the `license +agreement `_. + +********* +Resources +********* + +- `User Guide `_ +- `Contributing Guide `_ +- `Language File Translations `_ +- `Community Forums `_ +- `Community Wiki `_ +- `Community Slack Channel `_ + +Report security issues to our `Security Panel `_ +or via our `page on HackerOne `_, thank you. + +*************** +Acknowledgement +*************** + +The CodeIgniter team would like to thank EllisLab, all the +contributors to the CodeIgniter project and you, the CodeIgniter user. diff --git a/system/.htaccess b/system/.htaccess new file mode 100644 index 0000000..97c65d2 --- /dev/null +++ b/system/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + \ No newline at end of file diff --git a/system/core/Benchmark.php b/system/core/Benchmark.php new file mode 100644 index 0000000..20ac2f5 --- /dev/null +++ b/system/core/Benchmark.php @@ -0,0 +1,134 @@ +marker[$name] = microtime(TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Elapsed time + * + * Calculates the time difference between two marked points. + * + * If the first parameter is empty this function instead returns the + * {elapsed_time} pseudo-variable. This permits the full system + * execution time to be shown in a template. The output class will + * swap the real value for this variable. + * + * @param string $point1 A particular marked point + * @param string $point2 A particular marked point + * @param int $decimals Number of decimal places + * + * @return string Calculated elapsed time on success, + * an '{elapsed_string}' if $point1 is empty + * or an empty string if $point1 is not found. + */ + public function elapsed_time($point1 = '', $point2 = '', $decimals = 4) + { + if ($point1 === '') + { + return '{elapsed_time}'; + } + + if ( ! isset($this->marker[$point1])) + { + return ''; + } + + if ( ! isset($this->marker[$point2])) + { + $this->marker[$point2] = microtime(TRUE); + } + + return number_format($this->marker[$point2] - $this->marker[$point1], $decimals); + } + + // -------------------------------------------------------------------- + + /** + * Memory Usage + * + * Simply returns the {memory_usage} marker. + * + * This permits it to be put it anywhere in a template + * without the memory being calculated until the end. + * The output class will swap the real value for this variable. + * + * @return string '{memory_usage}' + */ + public function memory_usage() + { + return '{memory_usage}'; + } + +} diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php new file mode 100644 index 0000000..56826dc --- /dev/null +++ b/system/core/CodeIgniter.php @@ -0,0 +1,560 @@ + '_ENV', 'G' => '_GET', 'P' => '_POST', 'C' => '_COOKIE', 'S' => '_SERVER') as $key => $superglobal) + { + if (strpos($_registered, $key) === FALSE) + { + continue; + } + + foreach (array_keys($$superglobal) as $var) + { + if (isset($GLOBALS[$var]) && ! in_array($var, $_protected, TRUE)) + { + $GLOBALS[$var] = NULL; + } + } + } + } +} + + +/* + * ------------------------------------------------------ + * Define a custom error handler so we can log PHP errors + * ------------------------------------------------------ + */ + set_error_handler('_error_handler'); + set_exception_handler('_exception_handler'); + register_shutdown_function('_shutdown_handler'); + +/* + * ------------------------------------------------------ + * Set the subclass_prefix + * ------------------------------------------------------ + * + * Normally the "subclass_prefix" is set in the config file. + * The subclass prefix allows CI to know if a core class is + * being extended via a library in the local application + * "libraries" folder. Since CI allows config items to be + * overridden via data set in the main index.php file, + * before proceeding we need to know if a subclass_prefix + * override exists. If so, we will set this value now, + * before any classes are loaded + * Note: Since the config file data is cached it doesn't + * hurt to load it here. + */ + if ( ! empty($assign_to_config['subclass_prefix'])) + { + get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix'])); + } + +/* + * ------------------------------------------------------ + * Should we use a Composer autoloader? + * ------------------------------------------------------ + */ + if ($composer_autoload = config_item('composer_autoload')) + { + if ($composer_autoload === TRUE) + { + file_exists(APPPATH.'vendor/autoload.php') + ? require_once(APPPATH.'vendor/autoload.php') + : log_message('error', '$config[\'composer_autoload\'] is set to TRUE but '.APPPATH.'vendor/autoload.php was not found.'); + } + elseif (file_exists($composer_autoload)) + { + require_once($composer_autoload); + } + else + { + log_message('error', 'Could not find the specified $config[\'composer_autoload\'] path: '.$composer_autoload); + } + } + +/* + * ------------------------------------------------------ + * Start the timer... tick tock tick tock... + * ------------------------------------------------------ + */ + $BM =& load_class('Benchmark', 'core'); + $BM->mark('total_execution_time_start'); + $BM->mark('loading_time:_base_classes_start'); + +/* + * ------------------------------------------------------ + * Instantiate the hooks class + * ------------------------------------------------------ + */ + $EXT =& load_class('Hooks', 'core'); + +/* + * ------------------------------------------------------ + * Is there a "pre_system" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('pre_system'); + +/* + * ------------------------------------------------------ + * Instantiate the config class + * ------------------------------------------------------ + * + * Note: It is important that Config is loaded first as + * most other classes depend on it either directly or by + * depending on another class that uses it. + * + */ + $CFG =& load_class('Config', 'core'); + + // Do we have any manually set config items in the index.php file? + if (isset($assign_to_config) && is_array($assign_to_config)) + { + foreach ($assign_to_config as $key => $value) + { + $CFG->set_item($key, $value); + } + } + +/* + * ------------------------------------------------------ + * Important charset-related stuff + * ------------------------------------------------------ + * + * Configure mbstring and/or iconv if they are enabled + * and set MB_ENABLED and ICONV_ENABLED constants, so + * that we don't repeatedly do extension_loaded() or + * function_exists() calls. + * + * Note: UTF-8 class depends on this. It used to be done + * in it's constructor, but it's _not_ class-specific. + * + */ + $charset = strtoupper(config_item('charset')); + ini_set('default_charset', $charset); + + if (extension_loaded('mbstring')) + { + define('MB_ENABLED', TRUE); + // mbstring.internal_encoding is deprecated starting with PHP 5.6 + // and it's usage triggers E_DEPRECATED messages. + @ini_set('mbstring.internal_encoding', $charset); + // This is required for mb_convert_encoding() to strip invalid characters. + // That's utilized by CI_Utf8, but it's also done for consistency with iconv. + mb_substitute_character('none'); + } + else + { + define('MB_ENABLED', FALSE); + } + + // There's an ICONV_IMPL constant, but the PHP manual says that using + // iconv's predefined constants is "strongly discouraged". + if (extension_loaded('iconv')) + { + define('ICONV_ENABLED', TRUE); + // iconv.internal_encoding is deprecated starting with PHP 5.6 + // and it's usage triggers E_DEPRECATED messages. + @ini_set('iconv.internal_encoding', $charset); + } + else + { + define('ICONV_ENABLED', FALSE); + } + + if (is_php('5.6')) + { + ini_set('php.internal_encoding', $charset); + } + +/* + * ------------------------------------------------------ + * Load compatibility features + * ------------------------------------------------------ + */ + + require_once(BASEPATH.'core/compat/mbstring.php'); + require_once(BASEPATH.'core/compat/hash.php'); + require_once(BASEPATH.'core/compat/password.php'); + require_once(BASEPATH.'core/compat/standard.php'); + +/* + * ------------------------------------------------------ + * Instantiate the UTF-8 class + * ------------------------------------------------------ + */ + $UNI =& load_class('Utf8', 'core'); + +/* + * ------------------------------------------------------ + * Instantiate the URI class + * ------------------------------------------------------ + */ + $URI =& load_class('URI', 'core'); + +/* + * ------------------------------------------------------ + * Instantiate the routing class and set the routing + * ------------------------------------------------------ + */ + $RTR =& load_class('Router', 'core', isset($routing) ? $routing : NULL); + +/* + * ------------------------------------------------------ + * Instantiate the output class + * ------------------------------------------------------ + */ + $OUT =& load_class('Output', 'core'); + +/* + * ------------------------------------------------------ + * Is there a valid cache file? If so, we're done... + * ------------------------------------------------------ + */ + if ($EXT->call_hook('cache_override') === FALSE && $OUT->_display_cache($CFG, $URI) === TRUE) + { + exit; + } + +/* + * ----------------------------------------------------- + * Load the security class for xss and csrf support + * ----------------------------------------------------- + */ + $SEC =& load_class('Security', 'core'); + +/* + * ------------------------------------------------------ + * Load the Input class and sanitize globals + * ------------------------------------------------------ + */ + $IN =& load_class('Input', 'core'); + +/* + * ------------------------------------------------------ + * Load the Language class + * ------------------------------------------------------ + */ + $LANG =& load_class('Lang', 'core'); + +/* + * ------------------------------------------------------ + * Load the app controller and local controller + * ------------------------------------------------------ + * + */ + // Load the base controller class + require_once BASEPATH.'core/Controller.php'; + + /** + * Reference to the CI_Controller method. + * + * Returns current CI instance object + * + * @return CI_Controller + */ + function &get_instance() + { + return CI_Controller::get_instance(); + } + + if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php')) + { + require_once APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'; + } + + // Set a mark point for benchmarking + $BM->mark('loading_time:_base_classes_end'); + +/* + * ------------------------------------------------------ + * Sanity checks + * ------------------------------------------------------ + * + * The Router class has already validated the request, + * leaving us with 3 options here: + * + * 1) an empty class name, if we reached the default + * controller, but it didn't exist; + * 2) a query string which doesn't go through a + * file_exists() check + * 3) a regular request for a non-existing page + * + * We handle all of these as a 404 error. + * + * Furthermore, none of the methods in the app controller + * or the loader class can be called via the URI, nor can + * controller methods that begin with an underscore. + */ + + $e404 = FALSE; + $class = ucfirst($RTR->class); + $method = $RTR->method; + + if (empty($class) OR ! file_exists(APPPATH.'controllers/'.$RTR->directory.$class.'.php')) + { + $e404 = TRUE; + } + else + { + require_once(APPPATH.'controllers/'.$RTR->directory.$class.'.php'); + + if ( ! class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method)) + { + $e404 = TRUE; + } + elseif (method_exists($class, '_remap')) + { + $params = array($method, array_slice($URI->rsegments, 2)); + $method = '_remap'; + } + elseif ( ! method_exists($class, $method)) + { + $e404 = TRUE; + } + /** + * DO NOT CHANGE THIS, NOTHING ELSE WORKS! + * + * - method_exists() returns true for non-public methods, which passes the previous elseif + * - is_callable() returns false for PHP 4-style constructors, even if there's a __construct() + * - method_exists($class, '__construct') won't work because CI_Controller::__construct() is inherited + * - People will only complain if this doesn't work, even though it is documented that it shouldn't. + * + * ReflectionMethod::isConstructor() is the ONLY reliable check, + * knowing which method will be executed as a constructor. + */ + else + { + $reflection = new ReflectionMethod($class, $method); + if ( ! $reflection->isPublic() OR $reflection->isConstructor()) + { + $e404 = TRUE; + } + } + } + + if ($e404) + { + if ( ! empty($RTR->routes['404_override'])) + { + if (sscanf($RTR->routes['404_override'], '%[^/]/%s', $error_class, $error_method) !== 2) + { + $error_method = 'index'; + } + + $error_class = ucfirst($error_class); + + if ( ! class_exists($error_class, FALSE)) + { + if (file_exists(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php')) + { + require_once(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php'); + $e404 = ! class_exists($error_class, FALSE); + } + // Were we in a directory? If so, check for a global override + elseif ( ! empty($RTR->directory) && file_exists(APPPATH.'controllers/'.$error_class.'.php')) + { + require_once(APPPATH.'controllers/'.$error_class.'.php'); + if (($e404 = ! class_exists($error_class, FALSE)) === FALSE) + { + $RTR->directory = ''; + } + } + } + else + { + $e404 = FALSE; + } + } + + // Did we reset the $e404 flag? If so, set the rsegments, starting from index 1 + if ( ! $e404) + { + $class = $error_class; + $method = $error_method; + + $URI->rsegments = array( + 1 => $class, + 2 => $method + ); + } + else + { + show_404($RTR->directory.$class.'/'.$method); + } + } + + if ($method !== '_remap') + { + $params = array_slice($URI->rsegments, 2); + } + +/* + * ------------------------------------------------------ + * Is there a "pre_controller" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('pre_controller'); + +/* + * ------------------------------------------------------ + * Instantiate the requested controller + * ------------------------------------------------------ + */ + // Mark a start point so we can benchmark the controller + $BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_start'); + + $CI = new $class(); + +/* + * ------------------------------------------------------ + * Is there a "post_controller_constructor" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('post_controller_constructor'); + +/* + * ------------------------------------------------------ + * Call the requested method + * ------------------------------------------------------ + */ + call_user_func_array(array(&$CI, $method), $params); + + // Mark a benchmark end point + $BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end'); + +/* + * ------------------------------------------------------ + * Is there a "post_controller" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('post_controller'); + +/* + * ------------------------------------------------------ + * Send the final rendered output to the browser + * ------------------------------------------------------ + */ + if ($EXT->call_hook('display_override') === FALSE) + { + $OUT->_display(); + } + +/* + * ------------------------------------------------------ + * Is there a "post_system" hook? + * ------------------------------------------------------ + */ + $EXT->call_hook('post_system'); diff --git a/system/core/Common.php b/system/core/Common.php new file mode 100644 index 0000000..a56cb14 --- /dev/null +++ b/system/core/Common.php @@ -0,0 +1,849 @@ +='); + } + + return $_is_php[$version]; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('is_really_writable')) +{ + /** + * Tests for file writability + * + * is_writable() returns TRUE on Windows servers when you really can't write to + * the file, based on the read-only attribute. is_writable() is also unreliable + * on Unix servers if safe_mode is on. + * + * @link https://bugs.php.net/bug.php?id=54709 + * @param string + * @return bool + */ + function is_really_writable($file) + { + // If we're on a Unix server with safe_mode off we call is_writable + if (DIRECTORY_SEPARATOR === '/' && (is_php('5.4') OR ! ini_get('safe_mode'))) + { + return is_writable($file); + } + + /* For Windows servers and safe_mode "on" installations we'll actually + * write a file then read it. Bah... + */ + if (is_dir($file)) + { + $file = rtrim($file, '/').'/'.md5(mt_rand()); + if (($fp = @fopen($file, 'ab')) === FALSE) + { + return FALSE; + } + + fclose($fp); + @chmod($file, 0777); + @unlink($file); + return TRUE; + } + elseif ( ! is_file($file) OR ($fp = @fopen($file, 'ab')) === FALSE) + { + return FALSE; + } + + fclose($fp); + return TRUE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('load_class')) +{ + /** + * Class registry + * + * This function acts as a singleton. If the requested class does not + * exist it is instantiated and set to a static variable. If it has + * previously been instantiated the variable is returned. + * + * @param string the class name being requested + * @param string the directory where the class should be found + * @param mixed an optional argument to pass to the class constructor + * @return object + */ + function &load_class($class, $directory = 'libraries', $param = NULL) + { + static $_classes = array(); + + // Does the class exist? If so, we're done... + if (isset($_classes[$class])) + { + return $_classes[$class]; + } + + $name = FALSE; + + // Look for the class first in the local application/libraries folder + // then in the native system/libraries folder + foreach (array(APPPATH, BASEPATH) as $path) + { + if (file_exists($path.$directory.'/'.$class.'.php')) + { + $name = 'CI_'.$class; + + if (class_exists($name, FALSE) === FALSE) + { + require_once($path.$directory.'/'.$class.'.php'); + } + + break; + } + } + + // Is the request a class extension? If so we load it too + if (file_exists(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php')) + { + $name = config_item('subclass_prefix').$class; + + if (class_exists($name, FALSE) === FALSE) + { + require_once(APPPATH.$directory.'/'.$name.'.php'); + } + } + + // Did we find the class? + if ($name === FALSE) + { + // Note: We use exit() rather than show_error() in order to avoid a + // self-referencing loop with the Exceptions class + set_status_header(503); + echo 'Unable to locate the specified class: '.$class.'.php'; + exit(5); // EXIT_UNK_CLASS + } + + // Keep track of what we just loaded + is_loaded($class); + + $_classes[$class] = isset($param) + ? new $name($param) + : new $name(); + return $_classes[$class]; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('is_loaded')) +{ + /** + * Keeps track of which libraries have been loaded. This function is + * called by the load_class() function above + * + * @param string + * @return array + */ + function &is_loaded($class = '') + { + static $_is_loaded = array(); + + if ($class !== '') + { + $_is_loaded[strtolower($class)] = $class; + } + + return $_is_loaded; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('get_config')) +{ + /** + * Loads the main config.php file + * + * This function lets us grab the config file even if the Config class + * hasn't been instantiated yet + * + * @param array + * @return array + */ + function &get_config(Array $replace = array()) + { + static $config; + + if (empty($config)) + { + $file_path = APPPATH.'config/config.php'; + $found = FALSE; + if (file_exists($file_path)) + { + $found = TRUE; + require($file_path); + } + + // Is the config file in the environment folder? + if (file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/config.php')) + { + require($file_path); + } + elseif ( ! $found) + { + set_status_header(503); + echo 'The configuration file does not exist.'; + exit(3); // EXIT_CONFIG + } + + // Does the $config array exist in the file? + if ( ! isset($config) OR ! is_array($config)) + { + set_status_header(503); + echo 'Your config file does not appear to be formatted correctly.'; + exit(3); // EXIT_CONFIG + } + } + + // Are any values being dynamically added or replaced? + foreach ($replace as $key => $val) + { + $config[$key] = $val; + } + + return $config; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('config_item')) +{ + /** + * Returns the specified config item + * + * @param string + * @return mixed + */ + function config_item($item) + { + static $_config; + + if (empty($_config)) + { + // references cannot be directly assigned to static variables, so we use an array + $_config[0] =& get_config(); + } + + return isset($_config[0][$item]) ? $_config[0][$item] : NULL; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('get_mimes')) +{ + /** + * Returns the MIME types array from config/mimes.php + * + * @return array + */ + function &get_mimes() + { + static $_mimes; + + if (empty($_mimes)) + { + $_mimes = file_exists(APPPATH.'config/mimes.php') + ? include(APPPATH.'config/mimes.php') + : array(); + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/mimes.php')) + { + $_mimes = array_merge($_mimes, include(APPPATH.'config/'.ENVIRONMENT.'/mimes.php')); + } + } + + return $_mimes; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('is_https')) +{ + /** + * Is HTTPS? + * + * Determines if the application is accessed via an encrypted + * (HTTPS) connection. + * + * @return bool + */ + function is_https() + { + if ( ! empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') + { + return TRUE; + } + elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') + { + return TRUE; + } + elseif ( ! empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off') + { + return TRUE; + } + + return FALSE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('is_cli')) +{ + + /** + * Is CLI? + * + * Test to see if a request was made from the command line. + * + * @return bool + */ + function is_cli() + { + return (PHP_SAPI === 'cli' OR defined('STDIN')); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('show_error')) +{ + /** + * Error Handler + * + * This function lets us invoke the exception class and + * display errors using the standard error template located + * in application/views/errors/error_general.php + * This function will send the error page directly to the + * browser and exit. + * + * @param string + * @param int + * @param string + * @return void + */ + function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered') + { + $status_code = abs($status_code); + if ($status_code < 100) + { + $exit_status = $status_code + 9; // 9 is EXIT__AUTO_MIN + $status_code = 500; + } + else + { + $exit_status = 1; // EXIT_ERROR + } + + $_error =& load_class('Exceptions', 'core'); + echo $_error->show_error($heading, $message, 'error_general', $status_code); + exit($exit_status); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('show_404')) +{ + /** + * 404 Page Handler + * + * This function is similar to the show_error() function above + * However, instead of the standard error template it displays + * 404 errors. + * + * @param string + * @param bool + * @return void + */ + function show_404($page = '', $log_error = TRUE) + { + $_error =& load_class('Exceptions', 'core'); + $_error->show_404($page, $log_error); + exit(4); // EXIT_UNKNOWN_FILE + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('log_message')) +{ + /** + * Error Logging Interface + * + * We use this as a simple mechanism to access the logging + * class and send messages to be logged. + * + * @param string the error level: 'error', 'debug' or 'info' + * @param string the error message + * @return void + */ + function log_message($level, $message) + { + static $_log; + + if ($_log === NULL) + { + // references cannot be directly assigned to static variables, so we use an array + $_log[0] =& load_class('Log', 'core'); + } + + $_log[0]->write_log($level, $message); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('set_status_header')) +{ + /** + * Set HTTP Status Header + * + * @param int the status code + * @param string + * @return void + */ + function set_status_header($code = 200, $text = '') + { + if (is_cli()) + { + return; + } + + if (empty($code) OR ! is_numeric($code)) + { + show_error('Status codes must be numeric', 500); + } + + if (empty($text)) + { + is_int($code) OR $code = (int) $code; + $stati = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 511 => 'Network Authentication Required', + ); + + if (isset($stati[$code])) + { + $text = $stati[$code]; + } + else + { + show_error('No status text available. Please check your status code number or supply your own message text.', 500); + } + } + + if (strpos(PHP_SAPI, 'cgi') === 0) + { + header('Status: '.$code.' '.$text, TRUE); + return; + } + + $server_protocol = (isset($_SERVER['SERVER_PROTOCOL']) && in_array($_SERVER['SERVER_PROTOCOL'], array('HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0'), TRUE)) + ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; + header($server_protocol.' '.$code.' '.$text, TRUE, $code); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('_error_handler')) +{ + /** + * Error Handler + * + * This is the custom error handler that is declared at the (relative) + * top of CodeIgniter.php. The main reason we use this is to permit + * PHP errors to be logged in our own log files since the user may + * not have access to server logs. Since this function effectively + * intercepts PHP errors, however, we also need to display errors + * based on the current error_reporting level. + * We do that with the use of a PHP error template. + * + * @param int $severity + * @param string $message + * @param string $filepath + * @param int $line + * @return void + */ + function _error_handler($severity, $message, $filepath, $line) + { + $is_error = (((E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity); + + // When an error occurred, set the status header to '500 Internal Server Error' + // to indicate to the client something went wrong. + // This can't be done within the $_error->show_php_error method because + // it is only called when the display_errors flag is set (which isn't usually + // the case in a production environment) or when errors are ignored because + // they are above the error_reporting threshold. + if ($is_error) + { + set_status_header(500); + } + + // Should we ignore the error? We'll get the current error_reporting + // level and add its bits with the severity bits to find out. + if (($severity & error_reporting()) !== $severity) + { + return; + } + + $_error =& load_class('Exceptions', 'core'); + $_error->log_exception($severity, $message, $filepath, $line); + + // Should we display the error? + if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) + { + $_error->show_php_error($severity, $message, $filepath, $line); + } + + // If the error is fatal, the execution of the script should be stopped because + // errors can't be recovered from. Halting the script conforms with PHP's + // default error handling. See http://www.php.net/manual/en/errorfunc.constants.php + if ($is_error) + { + exit(1); // EXIT_ERROR + } + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('_exception_handler')) +{ + /** + * Exception Handler + * + * Sends uncaught exceptions to the logger and displays them + * only if display_errors is On so that they don't show up in + * production environments. + * + * @param Exception $exception + * @return void + */ + function _exception_handler($exception) + { + $_error =& load_class('Exceptions', 'core'); + $_error->log_exception('error', 'Exception: '.$exception->getMessage(), $exception->getFile(), $exception->getLine()); + + is_cli() OR set_status_header(500); + // Should we display the error? + if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) + { + $_error->show_exception($exception); + } + + exit(1); // EXIT_ERROR + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('_shutdown_handler')) +{ + /** + * Shutdown Handler + * + * This is the shutdown handler that is declared at the top + * of CodeIgniter.php. The main reason we use this is to simulate + * a complete custom exception handler. + * + * E_STRICT is purposively neglected because such events may have + * been caught. Duplication or none? None is preferred for now. + * + * @link http://insomanic.me.uk/post/229851073/php-trick-catching-fatal-errors-e-error-with-a + * @return void + */ + function _shutdown_handler() + { + $last_error = error_get_last(); + if (isset($last_error) && + ($last_error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING))) + { + _error_handler($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']); + } + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('remove_invisible_characters')) +{ + /** + * Remove Invisible Characters + * + * This prevents sandwiching null characters + * between ascii characters, like Java\0script. + * + * @param string + * @param bool + * @return string + */ + function remove_invisible_characters($str, $url_encoded = TRUE) + { + $non_displayables = array(); + + // every control character except newline (dec 10), + // carriage return (dec 13) and horizontal tab (dec 09) + if ($url_encoded) + { + $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15 + $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31 + $non_displayables[] = '/%7f/i'; // url encoded 127 + } + + $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 + + do + { + $str = preg_replace($non_displayables, '', $str, -1, $count); + } + while ($count); + + return $str; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('html_escape')) +{ + /** + * Returns HTML escaped variable. + * + * @param mixed $var The input string or array of strings to be escaped. + * @param bool $double_encode $double_encode set to FALSE prevents escaping twice. + * @return mixed The escaped string or array of strings as a result. + */ + function html_escape($var, $double_encode = TRUE) + { + if (empty($var)) + { + return $var; + } + + if (is_array($var)) + { + foreach (array_keys($var) as $key) + { + $var[$key] = html_escape($var[$key], $double_encode); + } + + return $var; + } + + return htmlspecialchars($var, ENT_QUOTES, config_item('charset'), $double_encode); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('_stringify_attributes')) +{ + /** + * Stringify attributes for use in HTML tags. + * + * Helper function used to convert a string, array, or object + * of attributes to a string. + * + * @param mixed string, array, object + * @param bool + * @return string + */ + function _stringify_attributes($attributes, $js = FALSE) + { + if (empty($attributes)) + { + return NULL; + } + + if (is_string($attributes)) + { + return ' '.$attributes; + } + + $attributes = (array) $attributes; + + $atts = ''; + foreach ($attributes as $key => $val) + { + $atts .= ($js) ? $key.'='.$val.',' : ' '.$key.'="'.$val.'"'; + } + + return rtrim($atts, ','); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('function_usable')) +{ + /** + * Function usable + * + * Executes a function_exists() check, and if the Suhosin PHP + * extension is loaded - checks whether the function that is + * checked might be disabled in there as well. + * + * This is useful as function_exists() will return FALSE for + * functions disabled via the *disable_functions* php.ini + * setting, but not for *suhosin.executor.func.blacklist* and + * *suhosin.executor.disable_eval*. These settings will just + * terminate script execution if a disabled function is executed. + * + * The above described behavior turned out to be a bug in Suhosin, + * but even though a fix was committed for 0.9.34 on 2012-02-12, + * that version is yet to be released. This function will therefore + * be just temporary, but would probably be kept for a few years. + * + * @link http://www.hardened-php.net/suhosin/ + * @param string $function_name Function to check for + * @return bool TRUE if the function exists and is safe to call, + * FALSE otherwise. + */ + function function_usable($function_name) + { + static $_suhosin_func_blacklist; + + if (function_exists($function_name)) + { + if ( ! isset($_suhosin_func_blacklist)) + { + $_suhosin_func_blacklist = extension_loaded('suhosin') + ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) + : array(); + } + + return ! in_array($function_name, $_suhosin_func_blacklist, TRUE); + } + + return FALSE; + } +} diff --git a/system/core/Config.php b/system/core/Config.php new file mode 100644 index 0000000..2454a9d --- /dev/null +++ b/system/core/Config.php @@ -0,0 +1,380 @@ +config =& get_config(); + + // Set the base_url automatically if none was provided + if (empty($this->config['base_url'])) + { + if (isset($_SERVER['SERVER_ADDR'])) + { + if (strpos($_SERVER['SERVER_ADDR'], ':') !== FALSE) + { + $server_addr = '['.$_SERVER['SERVER_ADDR'].']'; + } + else + { + $server_addr = $_SERVER['SERVER_ADDR']; + } + + $base_url = (is_https() ? 'https' : 'http').'://'.$server_addr + .substr($_SERVER['SCRIPT_NAME'], 0, strpos($_SERVER['SCRIPT_NAME'], basename($_SERVER['SCRIPT_FILENAME']))); + } + else + { + $base_url = 'http://localhost/'; + } + + $this->set_item('base_url', $base_url); + } + + log_message('info', 'Config Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Load Config File + * + * @param string $file Configuration file name + * @param bool $use_sections Whether configuration values should be loaded into their own section + * @param bool $fail_gracefully Whether to just return FALSE or display an error message + * @return bool TRUE if the file was loaded correctly or FALSE on failure + */ + public function load($file = '', $use_sections = FALSE, $fail_gracefully = FALSE) + { + $file = ($file === '') ? 'config' : str_replace('.php', '', $file); + $loaded = FALSE; + + foreach ($this->_config_paths as $path) + { + foreach (array($file, ENVIRONMENT.DIRECTORY_SEPARATOR.$file) as $location) + { + $file_path = $path.'config/'.$location.'.php'; + if (in_array($file_path, $this->is_loaded, TRUE)) + { + return TRUE; + } + + if ( ! file_exists($file_path)) + { + continue; + } + + include($file_path); + + if ( ! isset($config) OR ! is_array($config)) + { + if ($fail_gracefully === TRUE) + { + return FALSE; + } + + show_error('Your '.$file_path.' file does not appear to contain a valid configuration array.'); + } + + if ($use_sections === TRUE) + { + $this->config[$file] = isset($this->config[$file]) + ? array_merge($this->config[$file], $config) + : $config; + } + else + { + $this->config = array_merge($this->config, $config); + } + + $this->is_loaded[] = $file_path; + $config = NULL; + $loaded = TRUE; + log_message('debug', 'Config file loaded: '.$file_path); + } + } + + if ($loaded === TRUE) + { + return TRUE; + } + elseif ($fail_gracefully === TRUE) + { + return FALSE; + } + + show_error('The configuration file '.$file.'.php does not exist.'); + } + + // -------------------------------------------------------------------- + + /** + * Fetch a config file item + * + * @param string $item Config item name + * @param string $index Index name + * @return string|null The configuration item or NULL if the item doesn't exist + */ + public function item($item, $index = '') + { + if ($index == '') + { + return isset($this->config[$item]) ? $this->config[$item] : NULL; + } + + return isset($this->config[$index], $this->config[$index][$item]) ? $this->config[$index][$item] : NULL; + } + + // -------------------------------------------------------------------- + + /** + * Fetch a config file item with slash appended (if not empty) + * + * @param string $item Config item name + * @return string|null The configuration item or NULL if the item doesn't exist + */ + public function slash_item($item) + { + if ( ! isset($this->config[$item])) + { + return NULL; + } + elseif (trim($this->config[$item]) === '') + { + return ''; + } + + return rtrim($this->config[$item], '/').'/'; + } + + // -------------------------------------------------------------------- + + /** + * Site URL + * + * Returns base_url . index_page [. uri_string] + * + * @uses CI_Config::_uri_string() + * + * @param string|string[] $uri URI string or an array of segments + * @param string $protocol + * @return string + */ + public function site_url($uri = '', $protocol = NULL) + { + $base_url = $this->slash_item('base_url'); + + if (isset($protocol)) + { + // For protocol-relative links + if ($protocol === '') + { + $base_url = substr($base_url, strpos($base_url, '//')); + } + else + { + $base_url = $protocol.substr($base_url, strpos($base_url, '://')); + } + } + + if (empty($uri)) + { + return $base_url.$this->item('index_page'); + } + + $uri = $this->_uri_string($uri); + + if ($this->item('enable_query_strings') === FALSE) + { + $suffix = isset($this->config['url_suffix']) ? $this->config['url_suffix'] : ''; + + if ($suffix !== '') + { + if (($offset = strpos($uri, '?')) !== FALSE) + { + $uri = substr($uri, 0, $offset).$suffix.substr($uri, $offset); + } + else + { + $uri .= $suffix; + } + } + + return $base_url.$this->slash_item('index_page').$uri; + } + elseif (strpos($uri, '?') === FALSE) + { + $uri = '?'.$uri; + } + + return $base_url.$this->item('index_page').$uri; + } + + // ------------------------------------------------------------- + + /** + * Base URL + * + * Returns base_url [. uri_string] + * + * @uses CI_Config::_uri_string() + * + * @param string|string[] $uri URI string or an array of segments + * @param string $protocol + * @return string + */ + public function base_url($uri = '', $protocol = NULL) + { + $base_url = $this->slash_item('base_url'); + + if (isset($protocol)) + { + // For protocol-relative links + if ($protocol === '') + { + $base_url = substr($base_url, strpos($base_url, '//')); + } + else + { + $base_url = $protocol.substr($base_url, strpos($base_url, '://')); + } + } + + return $base_url.$this->_uri_string($uri); + } + + // ------------------------------------------------------------- + + /** + * Build URI string + * + * @used-by CI_Config::site_url() + * @used-by CI_Config::base_url() + * + * @param string|string[] $uri URI string or an array of segments + * @return string + */ + protected function _uri_string($uri) + { + if ($this->item('enable_query_strings') === FALSE) + { + is_array($uri) && $uri = implode('/', $uri); + return ltrim($uri, '/'); + } + elseif (is_array($uri)) + { + return http_build_query($uri); + } + + return $uri; + } + + // -------------------------------------------------------------------- + + /** + * System URL + * + * @deprecated 3.0.0 Encourages insecure practices + * @return string + */ + public function system_url() + { + $x = explode('/', preg_replace('|/*(.+?)/*$|', '\\1', BASEPATH)); + return $this->slash_item('base_url').end($x).'/'; + } + + // -------------------------------------------------------------------- + + /** + * Set a config file item + * + * @param string $item Config item key + * @param string $value Config item value + * @return void + */ + public function set_item($item, $value) + { + $this->config[$item] = $value; + } + +} diff --git a/system/core/Controller.php b/system/core/Controller.php new file mode 100644 index 0000000..aeccd60 --- /dev/null +++ b/system/core/Controller.php @@ -0,0 +1,104 @@ + $class) + { + $this->$var =& load_class($class); + } + + $this->load =& load_class('Loader', 'core'); + $this->load->initialize(); + log_message('info', 'Controller Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Get the CI singleton + * + * @static + * @return object + */ + public static function &get_instance() + { + return self::$instance; + } + +} diff --git a/system/core/Exceptions.php b/system/core/Exceptions.php new file mode 100644 index 0000000..b1bc2de --- /dev/null +++ b/system/core/Exceptions.php @@ -0,0 +1,275 @@ + 'Error', + E_WARNING => 'Warning', + E_PARSE => 'Parsing Error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core Error', + E_CORE_WARNING => 'Core Warning', + E_COMPILE_ERROR => 'Compile Error', + E_COMPILE_WARNING => 'Compile Warning', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice' + ); + + /** + * Class constructor + * + * @return void + */ + public function __construct() + { + $this->ob_level = ob_get_level(); + // Note: Do not log messages from this constructor. + } + + // -------------------------------------------------------------------- + + /** + * Exception Logger + * + * Logs PHP generated error messages + * + * @param int $severity Log level + * @param string $message Error message + * @param string $filepath File path + * @param int $line Line number + * @return void + */ + public function log_exception($severity, $message, $filepath, $line) + { + $severity = isset($this->levels[$severity]) ? $this->levels[$severity] : $severity; + log_message('error', 'Severity: '.$severity.' --> '.$message.' '.$filepath.' '.$line); + } + + // -------------------------------------------------------------------- + + /** + * 404 Error Handler + * + * @uses CI_Exceptions::show_error() + * + * @param string $page Page URI + * @param bool $log_error Whether to log the error + * @return void + */ + public function show_404($page = '', $log_error = TRUE) + { + if (is_cli()) + { + $heading = 'Not Found'; + $message = 'The controller/method pair you requested was not found.'; + } + else + { + $heading = '404 Page Not Found'; + $message = 'The page you requested was not found.'; + } + + // By default we log this, but allow a dev to skip it + if ($log_error) + { + log_message('error', $heading.': '.$page); + } + + echo $this->show_error($heading, $message, 'error_404', 404); + exit(4); // EXIT_UNKNOWN_FILE + } + + // -------------------------------------------------------------------- + + /** + * General Error Page + * + * Takes an error message as input (either as a string or an array) + * and displays it using the specified template. + * + * @param string $heading Page heading + * @param string|string[] $message Error message + * @param string $template Template name + * @param int $status_code (default: 500) + * + * @return string Error page output + */ + public function show_error($heading, $message, $template = 'error_general', $status_code = 500) + { + $templates_path = config_item('error_views_path'); + if (empty($templates_path)) + { + $templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR; + } + + if (is_cli()) + { + $message = "\t".(is_array($message) ? implode("\n\t", $message) : $message); + $template = 'cli'.DIRECTORY_SEPARATOR.$template; + } + else + { + set_status_header($status_code); + $message = '

'.(is_array($message) ? implode('

', $message) : $message).'

'; + $template = 'html'.DIRECTORY_SEPARATOR.$template; + } + + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_flush(); + } + ob_start(); + include($templates_path.$template.'.php'); + $buffer = ob_get_contents(); + ob_end_clean(); + return $buffer; + } + + // -------------------------------------------------------------------- + + public function show_exception($exception) + { + $templates_path = config_item('error_views_path'); + if (empty($templates_path)) + { + $templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR; + } + + $message = $exception->getMessage(); + if (empty($message)) + { + $message = '(null)'; + } + + if (is_cli()) + { + $templates_path .= 'cli'.DIRECTORY_SEPARATOR; + } + else + { + $templates_path .= 'html'.DIRECTORY_SEPARATOR; + } + + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_flush(); + } + + ob_start(); + include($templates_path.'error_exception.php'); + $buffer = ob_get_contents(); + ob_end_clean(); + echo $buffer; + } + + // -------------------------------------------------------------------- + + /** + * Native PHP error handler + * + * @param int $severity Error level + * @param string $message Error message + * @param string $filepath File path + * @param int $line Line number + * @return void + */ + public function show_php_error($severity, $message, $filepath, $line) + { + $templates_path = config_item('error_views_path'); + if (empty($templates_path)) + { + $templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR; + } + + $severity = isset($this->levels[$severity]) ? $this->levels[$severity] : $severity; + + // For safety reasons we don't show the full file path in non-CLI requests + if ( ! is_cli()) + { + $filepath = str_replace('\\', '/', $filepath); + if (FALSE !== strpos($filepath, '/')) + { + $x = explode('/', $filepath); + $filepath = $x[count($x)-2].'/'.end($x); + } + + $template = 'html'.DIRECTORY_SEPARATOR.'error_php'; + } + else + { + $template = 'cli'.DIRECTORY_SEPARATOR.'error_php'; + } + + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_flush(); + } + ob_start(); + include($templates_path.$template.'.php'); + $buffer = ob_get_contents(); + ob_end_clean(); + echo $buffer; + } + +} diff --git a/system/core/Hooks.php b/system/core/Hooks.php new file mode 100644 index 0000000..2246bbc --- /dev/null +++ b/system/core/Hooks.php @@ -0,0 +1,267 @@ +item('enable_hooks') === FALSE) + { + return; + } + + // Grab the "hooks" definition file. + if (file_exists(APPPATH.'config/hooks.php')) + { + include(APPPATH.'config/hooks.php'); + } + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/hooks.php')) + { + include(APPPATH.'config/'.ENVIRONMENT.'/hooks.php'); + } + + // If there are no hooks, we're done. + if ( ! isset($hook) OR ! is_array($hook)) + { + return; + } + + $this->hooks =& $hook; + $this->enabled = TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Call Hook + * + * Calls a particular hook. Called by CodeIgniter.php. + * + * @uses CI_Hooks::_run_hook() + * + * @param string $which Hook name + * @return bool TRUE on success or FALSE on failure + */ + public function call_hook($which = '') + { + if ( ! $this->enabled OR ! isset($this->hooks[$which])) + { + return FALSE; + } + + if (is_array($this->hooks[$which]) && ! isset($this->hooks[$which]['function'])) + { + foreach ($this->hooks[$which] as $val) + { + $this->_run_hook($val); + } + } + else + { + $this->_run_hook($this->hooks[$which]); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Run Hook + * + * Runs a particular hook + * + * @param array $data Hook details + * @return bool TRUE on success or FALSE on failure + */ + protected function _run_hook($data) + { + // Closures/lambda functions and array($object, 'method') callables + if (is_callable($data)) + { + is_array($data) + ? $data[0]->{$data[1]}() + : $data(); + + return TRUE; + } + elseif ( ! is_array($data)) + { + return FALSE; + } + + // ----------------------------------- + // Safety - Prevents run-away loops + // ----------------------------------- + + // If the script being called happens to have the same + // hook call within it a loop can happen + if ($this->_in_progress === TRUE) + { + return; + } + + // ----------------------------------- + // Set file path + // ----------------------------------- + + if ( ! isset($data['filepath'], $data['filename'])) + { + return FALSE; + } + + $filepath = APPPATH.$data['filepath'].'/'.$data['filename']; + + if ( ! file_exists($filepath)) + { + return FALSE; + } + + // Determine and class and/or function names + $class = empty($data['class']) ? FALSE : $data['class']; + $function = empty($data['function']) ? FALSE : $data['function']; + $params = isset($data['params']) ? $data['params'] : ''; + + if (empty($function)) + { + return FALSE; + } + + // Set the _in_progress flag + $this->_in_progress = TRUE; + + // Call the requested class and/or function + if ($class !== FALSE) + { + // The object is stored? + if (isset($this->_objects[$class])) + { + if (method_exists($this->_objects[$class], $function)) + { + $this->_objects[$class]->$function($params); + } + else + { + return $this->_in_progress = FALSE; + } + } + else + { + class_exists($class, FALSE) OR require_once($filepath); + + if ( ! class_exists($class, FALSE) OR ! method_exists($class, $function)) + { + return $this->_in_progress = FALSE; + } + + // Store the object and execute the method + $this->_objects[$class] = new $class(); + $this->_objects[$class]->$function($params); + } + } + else + { + function_exists($function) OR require_once($filepath); + + if ( ! function_exists($function)) + { + return $this->_in_progress = FALSE; + } + + $function($params); + } + + $this->_in_progress = FALSE; + return TRUE; + } + +} diff --git a/system/core/Input.php b/system/core/Input.php new file mode 100644 index 0000000..eba5f67 --- /dev/null +++ b/system/core/Input.php @@ -0,0 +1,937 @@ +_allow_get_array = (config_item('allow_get_array') !== FALSE); + $this->_enable_xss = (config_item('global_xss_filtering') === TRUE); + $this->_enable_csrf = (config_item('csrf_protection') === TRUE); + $this->_standardize_newlines = (bool) config_item('standardize_newlines'); + + $this->security =& load_class('Security', 'core'); + + // Do we need the UTF-8 class? + if (UTF8_ENABLED === TRUE) + { + $this->uni =& load_class('Utf8', 'core'); + } + + // Sanitize global arrays + $this->_sanitize_globals(); + + // CSRF Protection check + if ($this->_enable_csrf === TRUE && ! is_cli()) + { + $this->security->csrf_verify(); + } + + log_message('info', 'Input Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Fetch from array + * + * Internal method used to retrieve values from global arrays. + * + * @param array &$array $_GET, $_POST, $_COOKIE, $_SERVER, etc. + * @param mixed $index Index for item to be fetched from $array + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + protected function _fetch_from_array(&$array, $index = NULL, $xss_clean = NULL) + { + is_bool($xss_clean) OR $xss_clean = $this->_enable_xss; + + // If $index is NULL, it means that the whole $array is requested + isset($index) OR $index = array_keys($array); + + // allow fetching multiple keys at once + if (is_array($index)) + { + $output = array(); + foreach ($index as $key) + { + $output[$key] = $this->_fetch_from_array($array, $key, $xss_clean); + } + + return $output; + } + + if (isset($array[$index])) + { + $value = $array[$index]; + } + elseif (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) // Does the index contain array notation + { + $value = $array; + for ($i = 0; $i < $count; $i++) + { + $key = trim($matches[0][$i], '[]'); + if ($key === '') // Empty notation will return the value as array + { + break; + } + + if (isset($value[$key])) + { + $value = $value[$key]; + } + else + { + return NULL; + } + } + } + else + { + return NULL; + } + + return ($xss_clean === TRUE) + ? $this->security->xss_clean($value) + : $value; + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the GET array + * + * @param mixed $index Index for item to be fetched from $_GET + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function get($index = NULL, $xss_clean = NULL) + { + return $this->_fetch_from_array($_GET, $index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the POST array + * + * @param mixed $index Index for item to be fetched from $_POST + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function post($index = NULL, $xss_clean = NULL) + { + return $this->_fetch_from_array($_POST, $index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from POST data with fallback to GET + * + * @param string $index Index for item to be fetched from $_POST or $_GET + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function post_get($index, $xss_clean = NULL) + { + return isset($_POST[$index]) + ? $this->post($index, $xss_clean) + : $this->get($index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from GET data with fallback to POST + * + * @param string $index Index for item to be fetched from $_GET or $_POST + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function get_post($index, $xss_clean = NULL) + { + return isset($_GET[$index]) + ? $this->get($index, $xss_clean) + : $this->post($index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the COOKIE array + * + * @param mixed $index Index for item to be fetched from $_COOKIE + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function cookie($index = NULL, $xss_clean = NULL) + { + return $this->_fetch_from_array($_COOKIE, $index, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Fetch an item from the SERVER array + * + * @param mixed $index Index for item to be fetched from $_SERVER + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function server($index, $xss_clean = NULL) + { + return $this->_fetch_from_array($_SERVER, $index, $xss_clean); + } + + // ------------------------------------------------------------------------ + + /** + * Fetch an item from the php://input stream + * + * Useful when you need to access PUT, DELETE or PATCH request data. + * + * @param string $index Index for item to be fetched + * @param bool $xss_clean Whether to apply XSS filtering + * @return mixed + */ + public function input_stream($index = NULL, $xss_clean = NULL) + { + // Prior to PHP 5.6, the input stream can only be read once, + // so we'll need to check if we have already done that first. + if ( ! is_array($this->_input_stream)) + { + // $this->raw_input_stream will trigger __get(). + parse_str($this->raw_input_stream, $this->_input_stream); + is_array($this->_input_stream) OR $this->_input_stream = array(); + } + + return $this->_fetch_from_array($this->_input_stream, $index, $xss_clean); + } + + // ------------------------------------------------------------------------ + + /** + * Set cookie + * + * Accepts an arbitrary number of parameters (up to 7) or an associative + * array in the first parameter containing all the values. + * + * @param string|mixed[] $name Cookie name or an array containing parameters + * @param string $value Cookie value + * @param int $expire Cookie expiration time in seconds + * @param string $domain Cookie domain (e.g.: '.yourdomain.com') + * @param string $path Cookie path (default: '/') + * @param string $prefix Cookie name prefix + * @param bool $secure Whether to only transfer cookies via SSL + * @param bool $httponly Whether to only makes the cookie accessible via HTTP (no javascript) + * @param string $samesite SameSite attribute + * @return void + */ + public function set_cookie($name, $value = '', $expire = '', $domain = '', $path = '/', $prefix = '', $secure = NULL, $httponly = NULL, $samesite = NULL) + { + if (is_array($name)) + { + // always leave 'name' in last place, as the loop will break otherwise, due to $$item + foreach (array('value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name', 'samesite') as $item) + { + if (isset($name[$item])) + { + $$item = $name[$item]; + } + } + } + + if ($prefix === '' && config_item('cookie_prefix') !== '') + { + $prefix = config_item('cookie_prefix'); + } + + if ($domain == '' && config_item('cookie_domain') != '') + { + $domain = config_item('cookie_domain'); + } + + if ($path === '/' && config_item('cookie_path') !== '/') + { + $path = config_item('cookie_path'); + } + + $secure = ($secure === NULL && config_item('cookie_secure') !== NULL) + ? (bool) config_item('cookie_secure') + : (bool) $secure; + + $httponly = ($httponly === NULL && config_item('cookie_httponly') !== NULL) + ? (bool) config_item('cookie_httponly') + : (bool) $httponly; + + if ( ! is_numeric($expire)) + { + $expire = time() - 86500; + } + else + { + $expire = ($expire > 0) ? time() + $expire : 0; + } + + isset($samesite) OR $samesite = config_item('cookie_samesite'); + if (isset($samesite)) + { + $samesite = ucfirst(strtolower($samesite)); + in_array($samesite, array('Lax', 'Strict', 'None'), TRUE) OR $samesite = 'Lax'; + } + else + { + $samesite = 'Lax'; + } + + if ($samesite === 'None' && ! $secure) + { + log_message('error', $name.' cookie sent with SameSite=None, but without Secure attribute.'); + } + + if ( ! is_php('7.3')) + { + $maxage = $expire - time(); + if ($maxage < 1) + { + $maxage = 0; + } + + $cookie_header = 'Set-Cookie: '.$prefix.$name.'='.rawurlencode($value); + $cookie_header .= ($expire === 0 ? '' : '; Expires='.gmdate('D, d-M-Y H:i:s T', $expire)).'; Max-Age='.$maxage; + $cookie_header .= '; Path='.$path.($domain !== '' ? '; Domain='.$domain : ''); + $cookie_header .= ($secure ? '; Secure' : '').($httponly ? '; HttpOnly' : '').'; SameSite='.$samesite; + header($cookie_header); + return; + } + + $setcookie_options = array( + 'expires' => $expire, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => $httponly, + 'samesite' => $samesite, + ); + setcookie($prefix.$name, $value, $setcookie_options); + } + + // -------------------------------------------------------------------- + + /** + * Fetch the IP Address + * + * Determines and validates the visitor's IP address. + * + * @return string IP address + */ + public function ip_address() + { + if ($this->ip_address !== FALSE) + { + return $this->ip_address; + } + + $proxy_ips = config_item('proxy_ips'); + if ( ! empty($proxy_ips) && ! is_array($proxy_ips)) + { + $proxy_ips = explode(',', str_replace(' ', '', $proxy_ips)); + } + + $this->ip_address = $this->server('REMOTE_ADDR'); + + if ($proxy_ips) + { + foreach (array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP') as $header) + { + if (($spoof = $this->server($header)) !== NULL) + { + // Some proxies typically list the whole chain of IP + // addresses through which the client has reached us. + // e.g. client_ip, proxy_ip1, proxy_ip2, etc. + sscanf($spoof, '%[^,]', $spoof); + + if ( ! $this->valid_ip($spoof)) + { + $spoof = NULL; + } + else + { + break; + } + } + } + + if ($spoof) + { + for ($i = 0, $c = count($proxy_ips); $i < $c; $i++) + { + // Check if we have an IP address or a subnet + if (strpos($proxy_ips[$i], '/') === FALSE) + { + // An IP address (and not a subnet) is specified. + // We can compare right away. + if ($proxy_ips[$i] === $this->ip_address) + { + $this->ip_address = $spoof; + break; + } + + continue; + } + + // We have a subnet ... now the heavy lifting begins + isset($separator) OR $separator = $this->valid_ip($this->ip_address, 'ipv6') ? ':' : '.'; + + // If the proxy entry doesn't match the IP protocol - skip it + if (strpos($proxy_ips[$i], $separator) === FALSE) + { + continue; + } + + // Convert the REMOTE_ADDR IP address to binary, if needed + if ( ! isset($ip, $sprintf)) + { + if ($separator === ':') + { + // Make sure we're have the "full" IPv6 format + $ip = explode(':', + str_replace('::', + str_repeat(':', 9 - substr_count($this->ip_address, ':')), + $this->ip_address + ) + ); + + for ($j = 0; $j < 8; $j++) + { + $ip[$j] = intval($ip[$j], 16); + } + + $sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b'; + } + else + { + $ip = explode('.', $this->ip_address); + $sprintf = '%08b%08b%08b%08b'; + } + + $ip = vsprintf($sprintf, $ip); + } + + // Split the netmask length off the network address + sscanf($proxy_ips[$i], '%[^/]/%d', $netaddr, $masklen); + + // Again, an IPv6 address is most likely in a compressed form + if ($separator === ':') + { + $netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr)); + for ($j = 0; $j < 8; $j++) + { + $netaddr[$j] = intval($netaddr[$j], 16); + } + } + else + { + $netaddr = explode('.', $netaddr); + } + + // Convert to binary and finally compare + if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0) + { + $this->ip_address = $spoof; + break; + } + } + } + } + + if ( ! $this->valid_ip($this->ip_address)) + { + return $this->ip_address = '0.0.0.0'; + } + + return $this->ip_address; + } + + // -------------------------------------------------------------------- + + /** + * Validate IP Address + * + * @param string $ip IP address + * @param string $which IP protocol: 'ipv4' or 'ipv6' + * @return bool + */ + public function valid_ip($ip, $which = '') + { + switch (strtolower($which)) + { + case 'ipv4': + $which = FILTER_FLAG_IPV4; + break; + case 'ipv6': + $which = FILTER_FLAG_IPV6; + break; + default: + $which = 0; + break; + } + + return (bool) filter_var($ip, FILTER_VALIDATE_IP, $which); + } + + // -------------------------------------------------------------------- + + /** + * Fetch User Agent string + * + * @return string|null User Agent string or NULL if it doesn't exist + */ + public function user_agent($xss_clean = NULL) + { + return $this->_fetch_from_array($_SERVER, 'HTTP_USER_AGENT', $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Sanitize Globals + * + * Internal method serving for the following purposes: + * + * - Unsets $_GET data, if query strings are not enabled + * - Cleans POST, COOKIE and SERVER data + * - Standardizes newline characters to PHP_EOL + * + * @return void + */ + protected function _sanitize_globals() + { + // Is $_GET data allowed? If not we'll set the $_GET to an empty array + if ($this->_allow_get_array === FALSE) + { + $_GET = array(); + } + elseif (is_array($_GET)) + { + foreach ($_GET as $key => $val) + { + $_GET[$this->_clean_input_keys($key)] = $this->_clean_input_data($val); + } + } + + // Clean $_POST Data + if (is_array($_POST)) + { + foreach ($_POST as $key => $val) + { + $_POST[$this->_clean_input_keys($key)] = $this->_clean_input_data($val); + } + } + + // Clean $_COOKIE Data + if (is_array($_COOKIE)) + { + // Also get rid of specially treated cookies that might be set by a server + // or silly application, that are of no use to a CI application anyway + // but that when present will trip our 'Disallowed Key Characters' alarm + // http://www.ietf.org/rfc/rfc2109.txt + // note that the key names below are single quoted strings, and are not PHP variables + unset( + $_COOKIE['$Version'], + $_COOKIE['$Path'], + $_COOKIE['$Domain'] + ); + + foreach ($_COOKIE as $key => $val) + { + if (($cookie_key = $this->_clean_input_keys($key)) !== FALSE) + { + $_COOKIE[$cookie_key] = $this->_clean_input_data($val); + } + else + { + unset($_COOKIE[$key]); + } + } + } + + // Sanitize PHP_SELF + $_SERVER['PHP_SELF'] = strip_tags($_SERVER['PHP_SELF']); + + log_message('debug', 'Global POST, GET and COOKIE data sanitized'); + } + + // -------------------------------------------------------------------- + + /** + * Clean Input Data + * + * Internal method that aids in escaping data and + * standardizing newline characters to PHP_EOL. + * + * @param string|string[] $str Input string(s) + * @return string + */ + protected function _clean_input_data($str) + { + if (is_array($str)) + { + $new_array = array(); + foreach (array_keys($str) as $key) + { + $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($str[$key]); + } + return $new_array; + } + + /* We strip slashes if magic quotes is on to keep things consistent + + NOTE: In PHP 5.4 get_magic_quotes_gpc() will always return 0 and + it will probably not exist in future versions at all. + */ + if ( ! is_php('5.4') && get_magic_quotes_gpc()) + { + $str = stripslashes($str); + } + + // Clean UTF-8 if supported + if (UTF8_ENABLED === TRUE) + { + $str = $this->uni->clean_string($str); + } + + // Remove control characters + $str = remove_invisible_characters($str, FALSE); + + // Standardize newlines if needed + if ($this->_standardize_newlines === TRUE) + { + return preg_replace('/(?:\r\n|[\r\n])/', PHP_EOL, $str); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Clean Keys + * + * Internal method that helps to prevent malicious users + * from trying to exploit keys we make sure that keys are + * only named with alpha-numeric text and a few other items. + * + * @param string $str Input string + * @param bool $fatal Whether to terminate script exection + * or to return FALSE if an invalid + * key is encountered + * @return string|bool + */ + protected function _clean_input_keys($str, $fatal = TRUE) + { + if ( ! preg_match('/^[a-z0-9:_\/|-]+$/i', $str)) + { + if ($fatal === TRUE) + { + return FALSE; + } + else + { + set_status_header(503); + echo 'Disallowed Key Characters.'; + exit(7); // EXIT_USER_INPUT + } + } + + // Clean UTF-8 if supported + if (UTF8_ENABLED === TRUE) + { + return $this->uni->clean_string($str); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Request Headers + * + * @param bool $xss_clean Whether to apply XSS filtering + * @return array + */ + public function request_headers($xss_clean = FALSE) + { + // If header is already defined, return it immediately + if ( ! empty($this->headers)) + { + return $this->_fetch_from_array($this->headers, NULL, $xss_clean); + } + + // In Apache, you can simply call apache_request_headers() + if (function_exists('apache_request_headers')) + { + $this->headers = apache_request_headers(); + } + else + { + isset($_SERVER['CONTENT_TYPE']) && $this->headers['Content-Type'] = $_SERVER['CONTENT_TYPE']; + + foreach ($_SERVER as $key => $val) + { + if (sscanf($key, 'HTTP_%s', $header) === 1) + { + // take SOME_HEADER and turn it into Some-Header + $header = str_replace('_', ' ', strtolower($header)); + $header = str_replace(' ', '-', ucwords($header)); + + $this->headers[$header] = $_SERVER[$key]; + } + } + } + + return $this->_fetch_from_array($this->headers, NULL, $xss_clean); + } + + // -------------------------------------------------------------------- + + /** + * Get Request Header + * + * Returns the value of a single member of the headers class member + * + * @param string $index Header name + * @param bool $xss_clean Whether to apply XSS filtering + * @return string|null The requested header on success or NULL on failure + */ + public function get_request_header($index, $xss_clean = FALSE) + { + static $headers; + + if ( ! isset($headers)) + { + empty($this->headers) && $this->request_headers(); + foreach ($this->headers as $key => $value) + { + $headers[strtolower($key)] = $value; + } + } + + $index = strtolower($index); + + if ( ! isset($headers[$index])) + { + return NULL; + } + + return ($xss_clean === TRUE) + ? $this->security->xss_clean($headers[$index]) + : $headers[$index]; + } + + // -------------------------------------------------------------------- + + /** + * Is AJAX request? + * + * Test to see if a request contains the HTTP_X_REQUESTED_WITH header. + * + * @return bool + */ + public function is_ajax_request() + { + return ( ! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + // -------------------------------------------------------------------- + + /** + * Is CLI request? + * + * Test to see if a request was made from the command line. + * + * @deprecated 3.0.0 Use is_cli() instead + * @return bool + */ + public function is_cli_request() + { + return is_cli(); + } + + // -------------------------------------------------------------------- + + /** + * Get Request Method + * + * Return the request method + * + * @param bool $upper Whether to return in upper or lower case + * (default: FALSE) + * @return string + */ + public function method($upper = FALSE) + { + return ($upper) + ? strtoupper($this->server('REQUEST_METHOD')) + : strtolower($this->server('REQUEST_METHOD')); + } + + // ------------------------------------------------------------------------ + + /** + * Magic __get() + * + * Allows read access to protected properties + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if ($name === 'raw_input_stream') + { + isset($this->_raw_input_stream) OR $this->_raw_input_stream = file_get_contents('php://input'); + return $this->_raw_input_stream; + } + elseif ($name === 'ip_address') + { + return $this->ip_address; + } + } + +} diff --git a/system/core/Lang.php b/system/core/Lang.php new file mode 100644 index 0000000..1829906 --- /dev/null +++ b/system/core/Lang.php @@ -0,0 +1,204 @@ +load($value, $idiom, $return, $add_suffix, $alt_path); + } + + return; + } + + $langfile = str_replace('.php', '', $langfile); + + if ($add_suffix === TRUE) + { + $langfile = preg_replace('/_lang$/', '', $langfile).'_lang'; + } + + $langfile .= '.php'; + + if (empty($idiom) OR ! preg_match('/^[a-z_-]+$/i', $idiom)) + { + $config =& get_config(); + $idiom = empty($config['language']) ? 'english' : $config['language']; + } + + if ($return === FALSE && isset($this->is_loaded[$langfile]) && $this->is_loaded[$langfile] === $idiom) + { + return; + } + + // Load the base file, so any others found can override it + $basepath = BASEPATH.'language/'.$idiom.'/'.$langfile; + if (($found = file_exists($basepath)) === TRUE) + { + include($basepath); + } + + // Do we have an alternative path to look in? + if ($alt_path !== '') + { + $alt_path .= 'language/'.$idiom.'/'.$langfile; + if (file_exists($alt_path)) + { + include($alt_path); + $found = TRUE; + } + } + else + { + foreach (get_instance()->load->get_package_paths(TRUE) as $package_path) + { + $package_path .= 'language/'.$idiom.'/'.$langfile; + if ($basepath !== $package_path && file_exists($package_path)) + { + include($package_path); + $found = TRUE; + break; + } + } + } + + if ($found !== TRUE) + { + show_error('Unable to load the requested language file: language/'.$idiom.'/'.$langfile); + } + + if ( ! isset($lang) OR ! is_array($lang)) + { + log_message('error', 'Language file contains no data: language/'.$idiom.'/'.$langfile); + + if ($return === TRUE) + { + return array(); + } + return; + } + + if ($return === TRUE) + { + return $lang; + } + + $this->is_loaded[$langfile] = $idiom; + $this->language = array_merge($this->language, $lang); + + log_message('info', 'Language file loaded: language/'.$idiom.'/'.$langfile); + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Language line + * + * Fetches a single line of text from the language array + * + * @param string $line Language line key + * @param bool $log_errors Whether to log an error message if the line is not found + * @return string Translation + */ + public function line($line, $log_errors = TRUE) + { + $value = isset($this->language[$line]) ? $this->language[$line] : FALSE; + + // Because killer robots like unicorns! + if ($value === FALSE && $log_errors === TRUE) + { + log_message('error', 'Could not find the language line "'.$line.'"'); + } + + return $value; + } + +} diff --git a/system/core/Loader.php b/system/core/Loader.php new file mode 100644 index 0000000..a70487e --- /dev/null +++ b/system/core/Loader.php @@ -0,0 +1,1416 @@ + TRUE); + + /** + * List of paths to load libraries from + * + * @var array + */ + protected $_ci_library_paths = array(APPPATH, BASEPATH); + + /** + * List of paths to load models from + * + * @var array + */ + protected $_ci_model_paths = array(APPPATH); + + /** + * List of paths to load helpers from + * + * @var array + */ + protected $_ci_helper_paths = array(APPPATH, BASEPATH); + + /** + * List of cached variables + * + * @var array + */ + protected $_ci_cached_vars = array(); + + /** + * List of loaded classes + * + * @var array + */ + protected $_ci_classes = array(); + + /** + * List of loaded models + * + * @var array + */ + protected $_ci_models = array(); + + /** + * List of loaded helpers + * + * @var array + */ + protected $_ci_helpers = array(); + + /** + * List of class name mappings + * + * @var array + */ + protected $_ci_varmap = array( + 'unit_test' => 'unit', + 'user_agent' => 'agent' + ); + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * Sets component load paths, gets the initial output buffering level. + * + * @return void + */ + public function __construct() + { + $this->_ci_ob_level = ob_get_level(); + $this->_ci_classes =& is_loaded(); + + log_message('info', 'Loader Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Initializer + * + * @todo Figure out a way to move this to the constructor + * without breaking *package_path*() methods. + * @uses CI_Loader::_ci_autoloader() + * @used-by CI_Controller::__construct() + * @return void + */ + public function initialize() + { + $this->_ci_autoloader(); + } + + // -------------------------------------------------------------------- + + /** + * Is Loaded + * + * A utility method to test if a class is in the self::$_ci_classes array. + * + * @used-by Mainly used by Form Helper function _get_validation_object(). + * + * @param string $class Class name to check for + * @return string|bool Class object name if loaded or FALSE + */ + public function is_loaded($class) + { + return array_search(ucfirst($class), $this->_ci_classes, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Library Loader + * + * Loads and instantiates libraries. + * Designed to be called from application controllers. + * + * @param mixed $library Library name + * @param array $params Optional parameters to pass to the library class constructor + * @param string $object_name An optional object name to assign to + * @return object + */ + public function library($library, $params = NULL, $object_name = NULL) + { + if (empty($library)) + { + return $this; + } + elseif (is_array($library)) + { + foreach ($library as $key => $value) + { + if (is_int($key)) + { + $this->library($value, $params); + } + else + { + $this->library($key, $params, $value); + } + } + + return $this; + } + + if ($params !== NULL && ! is_array($params)) + { + $params = NULL; + } + + $this->_ci_load_library($library, $params, $object_name); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Model Loader + * + * Loads and instantiates models. + * + * @param mixed $model Model name + * @param string $name An optional object name to assign to + * @param bool $db_conn An optional database connection configuration to initialize + * @return object + */ + public function model($model, $name = '', $db_conn = FALSE) + { + if (empty($model)) + { + return $this; + } + elseif (is_array($model)) + { + foreach ($model as $key => $value) + { + is_int($key) ? $this->model($value, '', $db_conn) : $this->model($key, $value, $db_conn); + } + + return $this; + } + + $path = ''; + + // Is the model in a sub-folder? If so, parse out the filename and path. + if (($last_slash = strrpos($model, '/')) !== FALSE) + { + // The path is in front of the last slash + $path = substr($model, 0, ++$last_slash); + + // And the model name behind it + $model = substr($model, $last_slash); + } + + if (empty($name)) + { + $name = $model; + } + + if (in_array($name, $this->_ci_models, TRUE)) + { + return $this; + } + + $CI =& get_instance(); + if (isset($CI->$name)) + { + throw new RuntimeException('The model name you are loading is the name of a resource that is already being used: '.$name); + } + + if ($db_conn !== FALSE && ! class_exists('CI_DB', FALSE)) + { + if ($db_conn === TRUE) + { + $db_conn = ''; + } + + $this->database($db_conn, FALSE, TRUE); + } + + // Note: All of the code under this condition used to be just: + // + // load_class('Model', 'core'); + // + // However, load_class() instantiates classes + // to cache them for later use and that prevents + // MY_Model from being an abstract class and is + // sub-optimal otherwise anyway. + if ( ! class_exists('CI_Model', FALSE)) + { + $app_path = APPPATH.'core'.DIRECTORY_SEPARATOR; + if (file_exists($app_path.'Model.php')) + { + require_once($app_path.'Model.php'); + if ( ! class_exists('CI_Model', FALSE)) + { + throw new RuntimeException($app_path."Model.php exists, but doesn't declare class CI_Model"); + } + + log_message('info', 'CI_Model class loaded'); + } + elseif ( ! class_exists('CI_Model', FALSE)) + { + require_once(BASEPATH.'core'.DIRECTORY_SEPARATOR.'Model.php'); + } + + $class = config_item('subclass_prefix').'Model'; + if (file_exists($app_path.$class.'.php')) + { + require_once($app_path.$class.'.php'); + if ( ! class_exists($class, FALSE)) + { + throw new RuntimeException($app_path.$class.".php exists, but doesn't declare class ".$class); + } + + log_message('info', config_item('subclass_prefix').'Model class loaded'); + } + } + + $model = ucfirst($model); + if ( ! class_exists($model, FALSE)) + { + foreach ($this->_ci_model_paths as $mod_path) + { + if ( ! file_exists($mod_path.'models/'.$path.$model.'.php')) + { + continue; + } + + require_once($mod_path.'models/'.$path.$model.'.php'); + if ( ! class_exists($model, FALSE)) + { + throw new RuntimeException($mod_path."models/".$path.$model.".php exists, but doesn't declare class ".$model); + } + + break; + } + + if ( ! class_exists($model, FALSE)) + { + throw new RuntimeException('Unable to locate the model you have specified: '.$model); + } + } + elseif ( ! is_subclass_of($model, 'CI_Model')) + { + throw new RuntimeException("Class ".$model." already exists and doesn't extend CI_Model"); + } + + $this->_ci_models[] = $name; + $model = new $model(); + $CI->$name = $model; + log_message('info', 'Model "'.get_class($model).'" initialized'); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Database Loader + * + * @param mixed $params Database configuration options + * @param bool $return Whether to return the database object + * @param bool $query_builder Whether to enable Query Builder + * (overrides the configuration setting) + * + * @return object|bool Database object if $return is set to TRUE, + * FALSE on failure, CI_Loader instance in any other case + */ + public function database($params = '', $return = FALSE, $query_builder = NULL) + { + // Grab the super object + $CI =& get_instance(); + + // Do we even need to load the database class? + if ($return === FALSE && $query_builder === NULL && isset($CI->db) && is_object($CI->db) && ! empty($CI->db->conn_id)) + { + return FALSE; + } + + require_once(BASEPATH.'database/DB.php'); + + if ($return === TRUE) + { + return DB($params, $query_builder); + } + + // Initialize the db variable. Needed to prevent + // reference errors with some configurations + $CI->db = ''; + + // Load the DB class + $CI->db =& DB($params, $query_builder); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Load the Database Utilities Class + * + * @param object $db Database object + * @param bool $return Whether to return the DB Utilities class object or not + * @return object + */ + public function dbutil($db = NULL, $return = FALSE) + { + $CI =& get_instance(); + + if ( ! is_object($db) OR ! ($db instanceof CI_DB)) + { + class_exists('CI_DB', FALSE) OR $this->database(); + $db =& $CI->db; + } + + require_once(BASEPATH.'database/DB_utility.php'); + require_once(BASEPATH.'database/drivers/'.$db->dbdriver.'/'.$db->dbdriver.'_utility.php'); + $class = 'CI_DB_'.$db->dbdriver.'_utility'; + + if ($return === TRUE) + { + return new $class($db); + } + + $CI->dbutil = new $class($db); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Load the Database Forge Class + * + * @param object $db Database object + * @param bool $return Whether to return the DB Forge class object or not + * @return object + */ + public function dbforge($db = NULL, $return = FALSE) + { + $CI =& get_instance(); + if ( ! is_object($db) OR ! ($db instanceof CI_DB)) + { + class_exists('CI_DB', FALSE) OR $this->database(); + $db =& $CI->db; + } + + require_once(BASEPATH.'database/DB_forge.php'); + require_once(BASEPATH.'database/drivers/'.$db->dbdriver.'/'.$db->dbdriver.'_forge.php'); + + if ( ! empty($db->subdriver)) + { + $driver_path = BASEPATH.'database/drivers/'.$db->dbdriver.'/subdrivers/'.$db->dbdriver.'_'.$db->subdriver.'_forge.php'; + if (file_exists($driver_path)) + { + require_once($driver_path); + $class = 'CI_DB_'.$db->dbdriver.'_'.$db->subdriver.'_forge'; + } + } + else + { + $class = 'CI_DB_'.$db->dbdriver.'_forge'; + } + + if ($return === TRUE) + { + return new $class($db); + } + + $CI->dbforge = new $class($db); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * View Loader + * + * Loads "view" files. + * + * @param string $view View name + * @param array $vars An associative array of data + * to be extracted for use in the view + * @param bool $return Whether to return the view output + * or leave it to the Output class + * @return object|string + */ + public function view($view, $vars = array(), $return = FALSE) + { + return $this->_ci_load(array('_ci_view' => $view, '_ci_vars' => $this->_ci_prepare_view_vars($vars), '_ci_return' => $return)); + } + + // -------------------------------------------------------------------- + + /** + * Generic File Loader + * + * @param string $path File path + * @param bool $return Whether to return the file output + * @return object|string + */ + public function file($path, $return = FALSE) + { + return $this->_ci_load(array('_ci_path' => $path, '_ci_return' => $return)); + } + + // -------------------------------------------------------------------- + + /** + * Set Variables + * + * Once variables are set they become available within + * the controller class and its "view" files. + * + * @param array|object|string $vars + * An associative array or object containing values + * to be set, or a value's name if string + * @param string $val Value to set, only used if $vars is a string + * @return object + */ + public function vars($vars, $val = '') + { + $vars = is_string($vars) + ? array($vars => $val) + : $this->_ci_prepare_view_vars($vars); + + foreach ($vars as $key => $val) + { + $this->_ci_cached_vars[$key] = $val; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Clear Cached Variables + * + * Clears the cached variables. + * + * @return CI_Loader + */ + public function clear_vars() + { + $this->_ci_cached_vars = array(); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get Variable + * + * Check if a variable is set and retrieve it. + * + * @param string $key Variable name + * @return mixed The variable or NULL if not found + */ + public function get_var($key) + { + return isset($this->_ci_cached_vars[$key]) ? $this->_ci_cached_vars[$key] : NULL; + } + + // -------------------------------------------------------------------- + + /** + * Get Variables + * + * Retrieves all loaded variables. + * + * @return array + */ + public function get_vars() + { + return $this->_ci_cached_vars; + } + + // -------------------------------------------------------------------- + + /** + * Helper Loader + * + * @param string|string[] $helpers Helper name(s) + * @return object + */ + public function helper($helpers = array()) + { + is_array($helpers) OR $helpers = array($helpers); + foreach ($helpers as &$helper) + { + $filename = basename($helper); + $filepath = ($filename === $helper) ? '' : substr($helper, 0, strlen($helper) - strlen($filename)); + $filename = strtolower(preg_replace('#(_helper)?(\.php)?$#i', '', $filename)).'_helper'; + $helper = $filepath.$filename; + + if (isset($this->_ci_helpers[$helper])) + { + continue; + } + + // Is this a helper extension request? + $ext_helper = config_item('subclass_prefix').$filename; + $ext_loaded = FALSE; + foreach ($this->_ci_helper_paths as $path) + { + if (file_exists($path.'helpers/'.$ext_helper.'.php')) + { + include_once($path.'helpers/'.$ext_helper.'.php'); + $ext_loaded = TRUE; + } + } + + // If we have loaded extensions - check if the base one is here + if ($ext_loaded === TRUE) + { + $base_helper = BASEPATH.'helpers/'.$helper.'.php'; + if ( ! file_exists($base_helper)) + { + show_error('Unable to load the requested file: helpers/'.$helper.'.php'); + } + + include_once($base_helper); + $this->_ci_helpers[$helper] = TRUE; + log_message('info', 'Helper loaded: '.$helper); + continue; + } + + // No extensions found ... try loading regular helpers and/or overrides + foreach ($this->_ci_helper_paths as $path) + { + if (file_exists($path.'helpers/'.$helper.'.php')) + { + include_once($path.'helpers/'.$helper.'.php'); + + $this->_ci_helpers[$helper] = TRUE; + log_message('info', 'Helper loaded: '.$helper); + break; + } + } + + // unable to load the helper + if ( ! isset($this->_ci_helpers[$helper])) + { + show_error('Unable to load the requested file: helpers/'.$helper.'.php'); + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Load Helpers + * + * An alias for the helper() method in case the developer has + * written the plural form of it. + * + * @uses CI_Loader::helper() + * @param string|string[] $helpers Helper name(s) + * @return object + */ + public function helpers($helpers = array()) + { + return $this->helper($helpers); + } + + // -------------------------------------------------------------------- + + /** + * Language Loader + * + * Loads language files. + * + * @param string|string[] $files List of language file names to load + * @param string Language name + * @return object + */ + public function language($files, $lang = '') + { + get_instance()->lang->load($files, $lang); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Config Loader + * + * Loads a config file (an alias for CI_Config::load()). + * + * @uses CI_Config::load() + * @param string $file Configuration file name + * @param bool $use_sections Whether configuration values should be loaded into their own section + * @param bool $fail_gracefully Whether to just return FALSE or display an error message + * @return bool TRUE if the file was loaded correctly or FALSE on failure + */ + public function config($file, $use_sections = FALSE, $fail_gracefully = FALSE) + { + return get_instance()->config->load($file, $use_sections, $fail_gracefully); + } + + // -------------------------------------------------------------------- + + /** + * Driver Loader + * + * Loads a driver library. + * + * @param string|string[] $library Driver name(s) + * @param array $params Optional parameters to pass to the driver + * @param string $object_name An optional object name to assign to + * + * @return object|bool Object or FALSE on failure if $library is a string + * and $object_name is set. CI_Loader instance otherwise. + */ + public function driver($library, $params = NULL, $object_name = NULL) + { + if (is_array($library)) + { + foreach ($library as $key => $value) + { + if (is_int($key)) + { + $this->driver($value, $params); + } + else + { + $this->driver($key, $params, $value); + } + } + + return $this; + } + elseif (empty($library)) + { + return FALSE; + } + + if ( ! class_exists('CI_Driver_Library', FALSE)) + { + // We aren't instantiating an object here, just making the base class available + require BASEPATH.'libraries/Driver.php'; + } + + // We can save the loader some time since Drivers will *always* be in a subfolder, + // and typically identically named to the library + if ( ! strpos($library, '/')) + { + $library = ucfirst($library).'/'.$library; + } + + return $this->library($library, $params, $object_name); + } + + // -------------------------------------------------------------------- + + /** + * Add Package Path + * + * Prepends a parent path to the library, model, helper and config + * path arrays. + * + * @see CI_Loader::$_ci_library_paths + * @see CI_Loader::$_ci_model_paths + * @see CI_Loader::$_ci_helper_paths + * @see CI_Config::$_config_paths + * + * @param string $path Path to add + * @param bool $view_cascade (default: TRUE) + * @return object + */ + public function add_package_path($path, $view_cascade = TRUE) + { + $path = rtrim($path, '/').'/'; + + array_unshift($this->_ci_library_paths, $path); + array_unshift($this->_ci_model_paths, $path); + array_unshift($this->_ci_helper_paths, $path); + + $this->_ci_view_paths = array($path.'views/' => $view_cascade) + $this->_ci_view_paths; + + // Add config file path + $config =& $this->_ci_get_component('config'); + $config->_config_paths[] = $path; + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get Package Paths + * + * Return a list of all package paths. + * + * @param bool $include_base Whether to include BASEPATH (default: FALSE) + * @return array + */ + public function get_package_paths($include_base = FALSE) + { + return ($include_base === TRUE) ? $this->_ci_library_paths : $this->_ci_model_paths; + } + + // -------------------------------------------------------------------- + + /** + * Remove Package Path + * + * Remove a path from the library, model, helper and/or config + * path arrays if it exists. If no path is provided, the most recently + * added path will be removed removed. + * + * @param string $path Path to remove + * @return object + */ + public function remove_package_path($path = '') + { + $config =& $this->_ci_get_component('config'); + + if ($path === '') + { + array_shift($this->_ci_library_paths); + array_shift($this->_ci_model_paths); + array_shift($this->_ci_helper_paths); + array_shift($this->_ci_view_paths); + array_pop($config->_config_paths); + } + else + { + $path = rtrim($path, '/').'/'; + foreach (array('_ci_library_paths', '_ci_model_paths', '_ci_helper_paths') as $var) + { + if (($key = array_search($path, $this->{$var})) !== FALSE) + { + unset($this->{$var}[$key]); + } + } + + if (isset($this->_ci_view_paths[$path.'views/'])) + { + unset($this->_ci_view_paths[$path.'views/']); + } + + if (($key = array_search($path, $config->_config_paths)) !== FALSE) + { + unset($config->_config_paths[$key]); + } + } + + // make sure the application default paths are still in the array + $this->_ci_library_paths = array_unique(array_merge($this->_ci_library_paths, array(APPPATH, BASEPATH))); + $this->_ci_helper_paths = array_unique(array_merge($this->_ci_helper_paths, array(APPPATH, BASEPATH))); + $this->_ci_model_paths = array_unique(array_merge($this->_ci_model_paths, array(APPPATH))); + $this->_ci_view_paths = array_merge($this->_ci_view_paths, array(APPPATH.'views/' => TRUE)); + $config->_config_paths = array_unique(array_merge($config->_config_paths, array(APPPATH))); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Data Loader + * + * Used to load views and files. + * + * Variables are prefixed with _ci_ to avoid symbol collision with + * variables made available to view files. + * + * @used-by CI_Loader::view() + * @used-by CI_Loader::file() + * @param array $_ci_data Data to load + * @return object + */ + protected function _ci_load($_ci_data) + { + // Set the default data variables + foreach (array('_ci_view', '_ci_vars', '_ci_path', '_ci_return') as $_ci_val) + { + $$_ci_val = isset($_ci_data[$_ci_val]) ? $_ci_data[$_ci_val] : FALSE; + } + + $file_exists = FALSE; + + // Set the path to the requested file + if (is_string($_ci_path) && $_ci_path !== '') + { + $_ci_x = explode('/', $_ci_path); + $_ci_file = end($_ci_x); + } + else + { + $_ci_ext = pathinfo($_ci_view, PATHINFO_EXTENSION); + $_ci_file = ($_ci_ext === '') ? $_ci_view.'.php' : $_ci_view; + + foreach ($this->_ci_view_paths as $_ci_view_file => $cascade) + { + if (file_exists($_ci_view_file.$_ci_file)) + { + $_ci_path = $_ci_view_file.$_ci_file; + $file_exists = TRUE; + break; + } + + if ( ! $cascade) + { + break; + } + } + } + + if ( ! $file_exists && ! file_exists($_ci_path)) + { + show_error('Unable to load the requested file: '.$_ci_file); + } + + // This allows anything loaded using $this->load (views, files, etc.) + // to become accessible from within the Controller and Model functions. + $_ci_CI =& get_instance(); + foreach (get_object_vars($_ci_CI) as $_ci_key => $_ci_var) + { + if ( ! isset($this->$_ci_key)) + { + $this->$_ci_key =& $_ci_CI->$_ci_key; + } + } + + /* + * Extract and cache variables + * + * You can either set variables using the dedicated $this->load->vars() + * function or via the second parameter of this function. We'll merge + * the two types and cache them so that views that are embedded within + * other views can have access to these variables. + */ + empty($_ci_vars) OR $this->_ci_cached_vars = array_merge($this->_ci_cached_vars, $_ci_vars); + extract($this->_ci_cached_vars); + + /* + * Buffer the output + * + * We buffer the output for two reasons: + * 1. Speed. You get a significant speed boost. + * 2. So that the final rendered template can be post-processed by + * the output class. Why do we need post processing? For one thing, + * in order to show the elapsed page load time. Unless we can + * intercept the content right before it's sent to the browser and + * then stop the timer it won't be accurate. + */ + ob_start(); + + // If the PHP installation does not support short tags we'll + // do a little string replacement, changing the short tags + // to standard PHP echo statements. + if ( ! is_php('5.4') && ! ini_get('short_open_tag') && config_item('rewrite_short_tags') === TRUE) + { + echo eval('?>'.preg_replace('/;*\s*\?>/', '; ?>', str_replace(' $this->_ci_ob_level + 1) + { + ob_end_flush(); + } + else + { + $_ci_CI->output->append_output(ob_get_contents()); + @ob_end_clean(); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Library Loader + * + * @used-by CI_Loader::library() + * @uses CI_Loader::_ci_init_library() + * + * @param string $class Class name to load + * @param mixed $params Optional parameters to pass to the class constructor + * @param string $object_name Optional object name to assign to + * @return void + */ + protected function _ci_load_library($class, $params = NULL, $object_name = NULL) + { + // Get the class name, and while we're at it trim any slashes. + // The directory path can be included as part of the class name, + // but we don't want a leading slash + $class = str_replace('.php', '', trim($class, '/')); + + // Was the path included with the class name? + // We look for a slash to determine this + if (($last_slash = strrpos($class, '/')) !== FALSE) + { + // Extract the path + $subdir = substr($class, 0, ++$last_slash); + + // Get the filename from the path + $class = substr($class, $last_slash); + } + else + { + $subdir = ''; + } + + $class = ucfirst($class); + + // Is this a stock library? There are a few special conditions if so ... + if (file_exists(BASEPATH.'libraries/'.$subdir.$class.'.php')) + { + return $this->_ci_load_stock_library($class, $subdir, $params, $object_name); + } + + // Safety: Was the class already loaded by a previous call? + if (class_exists($class, FALSE)) + { + $property = $object_name; + if (empty($property)) + { + $property = strtolower($class); + isset($this->_ci_varmap[$property]) && $property = $this->_ci_varmap[$property]; + } + + $CI =& get_instance(); + if (isset($CI->$property)) + { + log_message('debug', $class.' class already loaded. Second attempt ignored.'); + return; + } + + return $this->_ci_init_library($class, '', $params, $object_name); + } + + // Let's search for the requested library file and load it. + foreach ($this->_ci_library_paths as $path) + { + // BASEPATH has already been checked for + if ($path === BASEPATH) + { + continue; + } + + $filepath = $path.'libraries/'.$subdir.$class.'.php'; + // Does the file exist? No? Bummer... + if ( ! file_exists($filepath)) + { + continue; + } + + include_once($filepath); + return $this->_ci_init_library($class, '', $params, $object_name); + } + + // One last attempt. Maybe the library is in a subdirectory, but it wasn't specified? + if ($subdir === '') + { + return $this->_ci_load_library($class.'/'.$class, $params, $object_name); + } + + // If we got this far we were unable to find the requested class. + log_message('error', 'Unable to load the requested class: '.$class); + show_error('Unable to load the requested class: '.$class); + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Stock Library Loader + * + * @used-by CI_Loader::_ci_load_library() + * @uses CI_Loader::_ci_init_library() + * + * @param string $library_name Library name to load + * @param string $file_path Path to the library filename, relative to libraries/ + * @param mixed $params Optional parameters to pass to the class constructor + * @param string $object_name Optional object name to assign to + * @return void + */ + protected function _ci_load_stock_library($library_name, $file_path, $params, $object_name) + { + $prefix = 'CI_'; + + if (class_exists($prefix.$library_name, FALSE)) + { + if (class_exists(config_item('subclass_prefix').$library_name, FALSE)) + { + $prefix = config_item('subclass_prefix'); + } + + $property = $object_name; + if (empty($property)) + { + $property = strtolower($library_name); + isset($this->_ci_varmap[$property]) && $property = $this->_ci_varmap[$property]; + } + + $CI =& get_instance(); + if ( ! isset($CI->$property)) + { + return $this->_ci_init_library($library_name, $prefix, $params, $object_name); + } + + log_message('debug', $library_name.' class already loaded. Second attempt ignored.'); + return; + } + + $paths = $this->_ci_library_paths; + array_pop($paths); // BASEPATH + array_pop($paths); // APPPATH (needs to be the first path checked) + array_unshift($paths, APPPATH); + + foreach ($paths as $path) + { + if (file_exists($path = $path.'libraries/'.$file_path.$library_name.'.php')) + { + // Override + include_once($path); + if (class_exists($prefix.$library_name, FALSE)) + { + return $this->_ci_init_library($library_name, $prefix, $params, $object_name); + } + + log_message('debug', $path.' exists, but does not declare '.$prefix.$library_name); + } + } + + include_once(BASEPATH.'libraries/'.$file_path.$library_name.'.php'); + + // Check for extensions + $subclass = config_item('subclass_prefix').$library_name; + foreach ($paths as $path) + { + if (file_exists($path = $path.'libraries/'.$file_path.$subclass.'.php')) + { + include_once($path); + if (class_exists($subclass, FALSE)) + { + $prefix = config_item('subclass_prefix'); + break; + } + + log_message('debug', $path.' exists, but does not declare '.$subclass); + } + } + + return $this->_ci_init_library($library_name, $prefix, $params, $object_name); + } + + // -------------------------------------------------------------------- + + /** + * Internal CI Library Instantiator + * + * @used-by CI_Loader::_ci_load_stock_library() + * @used-by CI_Loader::_ci_load_library() + * + * @param string $class Class name + * @param string $prefix Class name prefix + * @param array|null|bool $config Optional configuration to pass to the class constructor: + * FALSE to skip; + * NULL to search in config paths; + * array containing configuration data + * @param string $object_name Optional object name to assign to + * @return void + */ + protected function _ci_init_library($class, $prefix, $config = FALSE, $object_name = NULL) + { + // Is there an associated config file for this class? Note: these should always be lowercase + if ($config === NULL) + { + // Fetch the config paths containing any package paths + $config_component = $this->_ci_get_component('config'); + + if (is_array($config_component->_config_paths)) + { + $found = FALSE; + foreach ($config_component->_config_paths as $path) + { + // We test for both uppercase and lowercase, for servers that + // are case-sensitive with regard to file names. Load global first, + // override with environment next + if (file_exists($path.'config/'.strtolower($class).'.php')) + { + include($path.'config/'.strtolower($class).'.php'); + $found = TRUE; + } + elseif (file_exists($path.'config/'.ucfirst(strtolower($class)).'.php')) + { + include($path.'config/'.ucfirst(strtolower($class)).'.php'); + $found = TRUE; + } + + if (file_exists($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php')) + { + include($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php'); + $found = TRUE; + } + elseif (file_exists($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php')) + { + include($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php'); + $found = TRUE; + } + + // Break on the first found configuration, thus package + // files are not overridden by default paths + if ($found === TRUE) + { + break; + } + } + } + } + + $class_name = $prefix.$class; + + // Is the class name valid? + if ( ! class_exists($class_name, FALSE)) + { + log_message('error', 'Non-existent class: '.$class_name); + show_error('Non-existent class: '.$class_name); + } + + // Set the variable name we will assign the class to + // Was a custom class name supplied? If so we'll use it + if (empty($object_name)) + { + $object_name = strtolower($class); + if (isset($this->_ci_varmap[$object_name])) + { + $object_name = $this->_ci_varmap[$object_name]; + } + } + + // Don't overwrite existing properties + $CI =& get_instance(); + if (isset($CI->$object_name)) + { + if ($CI->$object_name instanceof $class_name) + { + log_message('debug', $class_name." has already been instantiated as '".$object_name."'. Second attempt aborted."); + return; + } + + show_error("Resource '".$object_name."' already exists and is not a ".$class_name." instance."); + } + + // Save the class name and object name + $this->_ci_classes[$object_name] = $class; + + // Instantiate the class + $CI->$object_name = isset($config) + ? new $class_name($config) + : new $class_name(); + } + + // -------------------------------------------------------------------- + + /** + * CI Autoloader + * + * Loads component listed in the config/autoload.php file. + * + * @used-by CI_Loader::initialize() + * @return void + */ + protected function _ci_autoloader() + { + if (file_exists(APPPATH.'config/autoload.php')) + { + include(APPPATH.'config/autoload.php'); + } + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/autoload.php')) + { + include(APPPATH.'config/'.ENVIRONMENT.'/autoload.php'); + } + + if ( ! isset($autoload)) + { + return; + } + + // Autoload packages + if (isset($autoload['packages'])) + { + foreach ($autoload['packages'] as $package_path) + { + $this->add_package_path($package_path); + } + } + + // Load any custom config file + if (count($autoload['config']) > 0) + { + foreach ($autoload['config'] as $val) + { + $this->config($val); + } + } + + // Autoload helpers and languages + foreach (array('helper', 'language') as $type) + { + if (isset($autoload[$type]) && count($autoload[$type]) > 0) + { + $this->$type($autoload[$type]); + } + } + + // Autoload drivers + if (isset($autoload['drivers'])) + { + $this->driver($autoload['drivers']); + } + + // Load libraries + if (isset($autoload['libraries']) && count($autoload['libraries']) > 0) + { + // Load the database driver. + if (in_array('database', $autoload['libraries'])) + { + $this->database(); + $autoload['libraries'] = array_diff($autoload['libraries'], array('database')); + } + + // Load all other libraries + $this->library($autoload['libraries']); + } + + // Autoload models + if (isset($autoload['model'])) + { + $this->model($autoload['model']); + } + } + + // -------------------------------------------------------------------- + + /** + * Prepare variables for _ci_vars, to be later extract()-ed inside views + * + * Converts objects to associative arrays and filters-out internal + * variable names (i.e. keys prefixed with '_ci_'). + * + * @param mixed $vars + * @return array + */ + protected function _ci_prepare_view_vars($vars) + { + if ( ! is_array($vars)) + { + $vars = is_object($vars) + ? get_object_vars($vars) + : array(); + } + + foreach (array_keys($vars) as $key) + { + if (strncmp($key, '_ci_', 4) === 0) + { + unset($vars[$key]); + } + } + + return $vars; + } + + // -------------------------------------------------------------------- + + /** + * CI Component getter + * + * Get a reference to a specific library or model. + * + * @param string $component Component name + * @return bool + */ + protected function &_ci_get_component($component) + { + $CI =& get_instance(); + return $CI->$component; + } +} diff --git a/system/core/Log.php b/system/core/Log.php new file mode 100644 index 0000000..ca3e38a --- /dev/null +++ b/system/core/Log.php @@ -0,0 +1,297 @@ + 1, 'DEBUG' => 2, 'INFO' => 3, 'ALL' => 4); + + /** + * mbstring.func_overload flag + * + * @var bool + */ + protected static $func_overload; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @return void + */ + public function __construct() + { + $config =& get_config(); + + isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload')); + + $this->_log_path = ($config['log_path'] !== '') ? $config['log_path'] : APPPATH.'logs/'; + $this->_file_ext = (isset($config['log_file_extension']) && $config['log_file_extension'] !== '') + ? ltrim($config['log_file_extension'], '.') : 'php'; + + file_exists($this->_log_path) OR mkdir($this->_log_path, 0755, TRUE); + + if ( ! is_dir($this->_log_path) OR ! is_really_writable($this->_log_path)) + { + $this->_enabled = FALSE; + } + + if (is_numeric($config['log_threshold'])) + { + $this->_threshold = (int) $config['log_threshold']; + } + elseif (is_array($config['log_threshold'])) + { + $this->_threshold = 0; + $this->_threshold_array = array_flip($config['log_threshold']); + } + + if ( ! empty($config['log_date_format'])) + { + $this->_date_fmt = $config['log_date_format']; + } + + if ( ! empty($config['log_file_permissions']) && is_int($config['log_file_permissions'])) + { + $this->_file_permissions = $config['log_file_permissions']; + } + } + + // -------------------------------------------------------------------- + + /** + * Write Log File + * + * Generally this function will be called using the global log_message() function + * + * @param string $level The error level: 'error', 'debug' or 'info' + * @param string $msg The error message + * @return bool + */ + public function write_log($level, $msg) + { + if ($this->_enabled === FALSE) + { + return FALSE; + } + + $level = strtoupper($level); + + if (( ! isset($this->_levels[$level]) OR ($this->_levels[$level] > $this->_threshold)) + && ! isset($this->_threshold_array[$this->_levels[$level]])) + { + return FALSE; + } + + $filepath = $this->_log_path.'log-'.date('Y-m-d').'.'.$this->_file_ext; + $message = ''; + + if ( ! file_exists($filepath)) + { + $newfile = TRUE; + // Only add protection to php files + if ($this->_file_ext === 'php') + { + $message .= "\n\n"; + } + } + + if ( ! $fp = @fopen($filepath, 'ab')) + { + return FALSE; + } + + flock($fp, LOCK_EX); + + // Instantiating DateTime with microseconds appended to initial date is needed for proper support of this format + if (strpos($this->_date_fmt, 'u') !== FALSE) + { + $microtime_full = microtime(TRUE); + $microtime_short = sprintf("%06d", ($microtime_full - floor($microtime_full)) * 1000000); + $date = new DateTime(date('Y-m-d H:i:s.'.$microtime_short, $microtime_full)); + $date = $date->format($this->_date_fmt); + } + else + { + $date = date($this->_date_fmt); + } + + $message .= $this->_format_line($level, $date, $msg); + + for ($written = 0, $length = self::strlen($message); $written < $length; $written += $result) + { + if (($result = fwrite($fp, self::substr($message, $written))) === FALSE) + { + break; + } + } + + flock($fp, LOCK_UN); + fclose($fp); + + if (isset($newfile) && $newfile === TRUE) + { + chmod($filepath, $this->_file_permissions); + } + + return is_int($result); + } + + // -------------------------------------------------------------------- + + /** + * Format the log line. + * + * This is for extensibility of log formatting + * If you want to change the log format, extend the CI_Log class and override this method + * + * @param string $level The error level + * @param string $date Formatted date string + * @param string $message The log message + * @return string Formatted log line with a new line character at the end + */ + protected function _format_line($level, $date, $message) + { + return $level.' - '.$date.' --> '.$message.PHP_EOL; + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe strlen() + * + * @param string $str + * @return int + */ + protected static function strlen($str) + { + return (self::$func_overload) + ? mb_strlen($str, '8bit') + : strlen($str); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function substr($str, $start, $length = NULL) + { + if (self::$func_overload) + { + // mb_substr($str, $start, null, '8bit') returns an empty + // string on PHP 5.3 + isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); + return mb_substr($str, $start, $length, '8bit'); + } + + return isset($length) + ? substr($str, $start, $length) + : substr($str, $start); + } +} diff --git a/system/core/Model.php b/system/core/Model.php new file mode 100644 index 0000000..b2bbbd4 --- /dev/null +++ b/system/core/Model.php @@ -0,0 +1,77 @@ +$key; + } + +} diff --git a/system/core/Output.php b/system/core/Output.php new file mode 100644 index 0000000..a629a09 --- /dev/null +++ b/system/core/Output.php @@ -0,0 +1,847 @@ +_zlib_oc = (bool) ini_get('zlib.output_compression'); + $this->_compress_output = ( + $this->_zlib_oc === FALSE + && config_item('compress_output') === TRUE + && extension_loaded('zlib') + ); + + isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload')); + + // Get mime types for later + $this->mimes =& get_mimes(); + + log_message('info', 'Output Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Get Output + * + * Returns the current output string. + * + * @return string + */ + public function get_output() + { + return $this->final_output; + } + + // -------------------------------------------------------------------- + + /** + * Set Output + * + * Sets the output string. + * + * @param string $output Output data + * @return CI_Output + */ + public function set_output($output) + { + $this->final_output = $output; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Append Output + * + * Appends data onto the output string. + * + * @param string $output Data to append + * @return CI_Output + */ + public function append_output($output) + { + $this->final_output .= $output; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Header + * + * Lets you set a server header which will be sent with the final output. + * + * Note: If a file is cached, headers will not be sent. + * @todo We need to figure out how to permit headers to be cached. + * + * @param string $header Header + * @param bool $replace Whether to replace the old header value, if already set + * @return CI_Output + */ + public function set_header($header, $replace = TRUE) + { + // If zlib.output_compression is enabled it will compress the output, + // but it will not modify the content-length header to compensate for + // the reduction, causing the browser to hang waiting for more data. + // We'll just skip content-length in those cases. + if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0) + { + return $this; + } + + $this->headers[] = array($header, $replace); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Content-Type Header + * + * @param string $mime_type Extension of the file we're outputting + * @param string $charset Character set (default: NULL) + * @return CI_Output + */ + public function set_content_type($mime_type, $charset = NULL) + { + if (strpos($mime_type, '/') === FALSE) + { + $extension = ltrim($mime_type, '.'); + + // Is this extension supported? + if (isset($this->mimes[$extension])) + { + $mime_type =& $this->mimes[$extension]; + + if (is_array($mime_type)) + { + $mime_type = current($mime_type); + } + } + } + + $this->mime_type = $mime_type; + + if (empty($charset)) + { + $charset = config_item('charset'); + } + + $header = 'Content-Type: '.$mime_type + .(empty($charset) ? '' : '; charset='.$charset); + + $this->headers[] = array($header, TRUE); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get Current Content-Type Header + * + * @return string 'text/html', if not already set + */ + public function get_content_type() + { + for ($i = 0, $c = count($this->headers); $i < $c; $i++) + { + if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1) + { + return $content_type; + } + } + + return 'text/html'; + } + + // -------------------------------------------------------------------- + + /** + * Get Header + * + * @param string $header + * @return string + */ + public function get_header($header) + { + // We only need [x][0] from our multi-dimensional array + $header_lines = array_map(function ($headers) + { + return array_shift($headers); + }, $this->headers); + + $headers = array_merge( + $header_lines, + headers_list() + ); + + if (empty($headers) OR empty($header)) + { + return NULL; + } + + // Count backwards, in order to get the last matching header + for ($c = count($headers) - 1; $c > -1; $c--) + { + if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0) + { + return trim(self::substr($headers[$c], $l+1)); + } + } + + return NULL; + } + + // -------------------------------------------------------------------- + + /** + * Set HTTP Status Header + * + * As of version 1.7.2, this is an alias for common function + * set_status_header(). + * + * @param int $code Status code (default: 200) + * @param string $text Optional message + * @return CI_Output + */ + public function set_status_header($code = 200, $text = '') + { + set_status_header($code, $text); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Enable/disable Profiler + * + * @param bool $val TRUE to enable or FALSE to disable + * @return CI_Output + */ + public function enable_profiler($val = TRUE) + { + $this->enable_profiler = is_bool($val) ? $val : TRUE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Profiler Sections + * + * Allows override of default/config settings for + * Profiler section display. + * + * @param array $sections Profiler sections + * @return CI_Output + */ + public function set_profiler_sections($sections) + { + if (isset($sections['query_toggle_count'])) + { + $this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count']; + unset($sections['query_toggle_count']); + } + + foreach ($sections as $section => $enable) + { + $this->_profiler_sections[$section] = ($enable !== FALSE); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Set Cache + * + * @param int $time Cache expiration time in minutes + * @return CI_Output + */ + public function cache($time) + { + $this->cache_expiration = is_numeric($time) ? $time : 0; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Display Output + * + * Processes and sends finalized output data to the browser along + * with any server headers and profile data. It also stops benchmark + * timers so the page rendering speed and memory usage can be shown. + * + * Note: All "view" data is automatically put into $this->final_output + * by controller class. + * + * @uses CI_Output::$final_output + * @param string $output Output data override + * @return void + */ + public function _display($output = '') + { + // Note: We use load_class() because we can't use $CI =& get_instance() + // since this function is sometimes called by the caching mechanism, + // which happens before the CI super object is available. + $BM =& load_class('Benchmark', 'core'); + $CFG =& load_class('Config', 'core'); + + // Grab the super object if we can. + if (class_exists('CI_Controller', FALSE)) + { + $CI =& get_instance(); + } + + // -------------------------------------------------------------------- + + // Set the output data + if ($output === '') + { + $output =& $this->final_output; + } + + // -------------------------------------------------------------------- + + // Do we need to write a cache file? Only if the controller does not have its + // own _output() method and we are not dealing with a cache file, which we + // can determine by the existence of the $CI object above + if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output')) + { + $this->_write_cache($output); + } + + // -------------------------------------------------------------------- + + // Parse out the elapsed time and memory usage, + // then swap the pseudo-variables with the data + + $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); + + if ($this->parse_exec_vars === TRUE) + { + $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; + $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); + } + + // -------------------------------------------------------------------- + + // Is compression requested? + if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed + && $this->_compress_output === TRUE + && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) + { + ob_start('ob_gzhandler'); + } + + // -------------------------------------------------------------------- + + // Are there any server headers to send? + if (count($this->headers) > 0) + { + foreach ($this->headers as $header) + { + @header($header[0], $header[1]); + } + } + + // -------------------------------------------------------------------- + + // Does the $CI object exist? + // If not we know we are dealing with a cache file so we'll + // simply echo out the data and exit. + if ( ! isset($CI)) + { + if ($this->_compress_output === TRUE) + { + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) + { + header('Content-Encoding: gzip'); + header('Content-Length: '.self::strlen($output)); + } + else + { + // User agent doesn't support gzip compression, + // so we'll have to decompress our cache + $output = gzinflate(self::substr($output, 10, -8)); + } + } + + echo $output; + log_message('info', 'Final output sent to browser'); + log_message('debug', 'Total execution time: '.$elapsed); + return; + } + + // -------------------------------------------------------------------- + + // Do we need to generate profile data? + // If so, load the Profile class and run it. + if ($this->enable_profiler === TRUE) + { + $CI->load->library('profiler'); + if ( ! empty($this->_profiler_sections)) + { + $CI->profiler->set_sections($this->_profiler_sections); + } + + // If the output data contains closing and tags + // we will remove them and add them back after we insert the profile data + $output = preg_replace('|.*?|is', '', $output, -1, $count).$CI->profiler->run(); + if ($count > 0) + { + $output .= ''; + } + } + + // Does the controller contain a function named _output()? + // If so send the output there. Otherwise, echo it. + if (method_exists($CI, '_output')) + { + $CI->_output($output); + } + else + { + echo $output; // Send it to the browser! + } + + log_message('info', 'Final output sent to browser'); + log_message('debug', 'Total execution time: '.$elapsed); + } + + // -------------------------------------------------------------------- + + /** + * Write Cache + * + * @param string $output Output data to cache + * @return void + */ + public function _write_cache($output) + { + $CI =& get_instance(); + $path = $CI->config->item('cache_path'); + $cache_path = ($path === '') ? APPPATH.'cache/' : $path; + + if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) + { + log_message('error', 'Unable to write cache file: '.$cache_path); + return; + } + + $uri = $CI->config->item('base_url') + .$CI->config->item('index_page') + .$CI->uri->uri_string(); + + if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) + { + if (is_array($cache_query_string)) + { + $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); + } + else + { + $uri .= '?'.$_SERVER['QUERY_STRING']; + } + } + + $cache_path .= md5($uri); + + if ( ! $fp = @fopen($cache_path, 'w+b')) + { + log_message('error', 'Unable to write cache file: '.$cache_path); + return; + } + + if ( ! flock($fp, LOCK_EX)) + { + log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); + fclose($fp); + return; + } + + // If output compression is enabled, compress the cache + // itself, so that we don't have to do that each time + // we're serving it + if ($this->_compress_output === TRUE) + { + $output = gzencode($output); + + if ($this->get_header('content-type') === NULL) + { + $this->set_content_type($this->mime_type); + } + } + + $expire = time() + ($this->cache_expiration * 60); + + // Put together our serialized info. + $cache_info = serialize(array( + 'expire' => $expire, + 'headers' => $this->headers + )); + + $output = $cache_info.'ENDCI--->'.$output; + + for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result) + { + if (($result = fwrite($fp, self::substr($output, $written))) === FALSE) + { + break; + } + } + + flock($fp, LOCK_UN); + fclose($fp); + + if ( ! is_int($result)) + { + @unlink($cache_path); + log_message('error', 'Unable to write the complete cache content at: '.$cache_path); + return; + } + + chmod($cache_path, 0640); + log_message('debug', 'Cache file written: '.$cache_path); + + // Send HTTP cache-control headers to browser to match file cache settings. + $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); + } + + // -------------------------------------------------------------------- + + /** + * Update/serve cached output + * + * @uses CI_Config + * @uses CI_URI + * + * @param object &$CFG CI_Config class instance + * @param object &$URI CI_URI class instance + * @return bool TRUE on success or FALSE on failure + */ + public function _display_cache(&$CFG, &$URI) + { + $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path'); + + // Build the file path. The file name is an MD5 hash of the full URI + $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string; + + if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) + { + if (is_array($cache_query_string)) + { + $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); + } + else + { + $uri .= '?'.$_SERVER['QUERY_STRING']; + } + } + + $filepath = $cache_path.md5($uri); + + if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb')) + { + return FALSE; + } + + flock($fp, LOCK_SH); + + $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : ''; + + flock($fp, LOCK_UN); + fclose($fp); + + // Look for embedded serialized file info. + if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match)) + { + return FALSE; + } + + $cache_info = unserialize($match[1]); + $expire = $cache_info['expire']; + + $last_modified = filemtime($filepath); + + // Has the file expired? + if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path)) + { + // If so we'll delete it. + @unlink($filepath); + log_message('debug', 'Cache file has expired. File deleted.'); + return FALSE; + } + + // Send the HTTP cache control headers + $this->set_cache_header($last_modified, $expire); + + // Add headers from cache file. + foreach ($cache_info['headers'] as $header) + { + $this->set_header($header[0], $header[1]); + } + + // Display the cache + $this->_display(self::substr($cache, self::strlen($match[0]))); + log_message('debug', 'Cache file is current. Sending it to browser.'); + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Delete cache + * + * @param string $uri URI string + * @return bool + */ + public function delete_cache($uri = '') + { + $CI =& get_instance(); + $cache_path = $CI->config->item('cache_path'); + if ($cache_path === '') + { + $cache_path = APPPATH.'cache/'; + } + + if ( ! is_dir($cache_path)) + { + log_message('error', 'Unable to find cache path: '.$cache_path); + return FALSE; + } + + if (empty($uri)) + { + $uri = $CI->uri->uri_string(); + + if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING'])) + { + if (is_array($cache_query_string)) + { + $uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string))); + } + else + { + $uri .= '?'.$_SERVER['QUERY_STRING']; + } + } + } + + $cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/')); + + if ( ! @unlink($cache_path)) + { + log_message('error', 'Unable to delete cache file for '.$uri); + return FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Set Cache Header + * + * Set the HTTP headers to match the server-side file cache settings + * in order to reduce bandwidth. + * + * @param int $last_modified Timestamp of when the page was last modified + * @param int $expiration Timestamp of when should the requested page expire from cache + * @return void + */ + public function set_cache_header($last_modified, $expiration) + { + $max_age = $expiration - $_SERVER['REQUEST_TIME']; + + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) + { + $this->set_status_header(304); + exit; + } + + header('Pragma: public'); + header('Cache-Control: max-age='.$max_age.', public'); + header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); + header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe strlen() + * + * @param string $str + * @return int + */ + protected static function strlen($str) + { + return (self::$func_overload) + ? mb_strlen($str, '8bit') + : strlen($str); + } + + // -------------------------------------------------------------------- + + /** + * Byte-safe substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + protected static function substr($str, $start, $length = NULL) + { + if (self::$func_overload) + { + // mb_substr($str, $start, null, '8bit') returns an empty + // string on PHP 5.3 + isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); + return mb_substr($str, $start, $length, '8bit'); + } + + return isset($length) + ? substr($str, $start, $length) + : substr($str, $start); + } +} diff --git a/system/core/Router.php b/system/core/Router.php new file mode 100644 index 0000000..ab1f44e --- /dev/null +++ b/system/core/Router.php @@ -0,0 +1,516 @@ +config =& load_class('Config', 'core'); + $this->uri =& load_class('URI', 'core'); + + $this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE); + + // If a directory override is configured, it has to be set before any dynamic routing logic + is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']); + $this->_set_routing(); + + // Set any routing overrides that may exist in the main index file + if (is_array($routing)) + { + empty($routing['controller']) OR $this->set_class($routing['controller']); + empty($routing['function']) OR $this->set_method($routing['function']); + } + + log_message('info', 'Router Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Set route mapping + * + * Determines what should be served based on the URI request, + * as well as any "routes" that have been set in the routing config file. + * + * @return void + */ + protected function _set_routing() + { + // Load the routes.php file. It would be great if we could + // skip this for enable_query_strings = TRUE, but then + // default_controller would be empty ... + if (file_exists(APPPATH.'config/routes.php')) + { + include(APPPATH.'config/routes.php'); + } + + if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php')) + { + include(APPPATH.'config/'.ENVIRONMENT.'/routes.php'); + } + + // Validate & get reserved routes + if (isset($route) && is_array($route)) + { + isset($route['default_controller']) && $this->default_controller = $route['default_controller']; + isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes']; + unset($route['default_controller'], $route['translate_uri_dashes']); + $this->routes = $route; + } + + // Are query strings enabled in the config file? Normally CI doesn't utilize query strings + // since URI segments are more search-engine friendly, but they can optionally be used. + // If this feature is enabled, we will gather the directory/class/method a little differently + if ($this->enable_query_strings) + { + // If the directory is set at this time, it means an override exists, so skip the checks + if ( ! isset($this->directory)) + { + $_d = $this->config->item('directory_trigger'); + $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : ''; + + if ($_d !== '') + { + $this->uri->filter_uri($_d); + $this->set_directory($_d); + } + } + + $_c = trim($this->config->item('controller_trigger')); + if ( ! empty($_GET[$_c])) + { + $this->uri->filter_uri($_GET[$_c]); + $this->set_class($_GET[$_c]); + + $_f = trim($this->config->item('function_trigger')); + if ( ! empty($_GET[$_f])) + { + $this->uri->filter_uri($_GET[$_f]); + $this->set_method($_GET[$_f]); + } + + $this->uri->rsegments = array( + 1 => $this->class, + 2 => $this->method + ); + } + else + { + $this->_set_default_controller(); + } + + // Routing rules don't apply to query strings and we don't need to detect + // directories, so we're done here + return; + } + + // Is there anything to parse? + if ($this->uri->uri_string !== '') + { + $this->_parse_routes(); + } + else + { + $this->_set_default_controller(); + } + } + + // -------------------------------------------------------------------- + + /** + * Set request route + * + * Takes an array of URI segments as input and sets the class/method + * to be called. + * + * @used-by CI_Router::_parse_routes() + * @param array $segments URI segments + * @return void + */ + protected function _set_request($segments = array()) + { + $segments = $this->_validate_request($segments); + // If we don't have any segments left - try the default controller; + // WARNING: Directories get shifted out of the segments array! + if (empty($segments)) + { + $this->_set_default_controller(); + return; + } + + if ($this->translate_uri_dashes === TRUE) + { + $segments[0] = str_replace('-', '_', $segments[0]); + if (isset($segments[1])) + { + $segments[1] = str_replace('-', '_', $segments[1]); + } + } + + $this->set_class($segments[0]); + if (isset($segments[1])) + { + $this->set_method($segments[1]); + } + else + { + $segments[1] = 'index'; + } + + array_unshift($segments, NULL); + unset($segments[0]); + $this->uri->rsegments = $segments; + } + + // -------------------------------------------------------------------- + + /** + * Set default controller + * + * @return void + */ + protected function _set_default_controller() + { + if (empty($this->default_controller)) + { + show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.'); + } + + // Is the method being specified? + if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2) + { + $method = 'index'; + } + + if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php')) + { + // This will trigger 404 later + return; + } + + $this->set_class($class); + $this->set_method($method); + + // Assign routed segments, index starting from 1 + $this->uri->rsegments = array( + 1 => $class, + 2 => $method + ); + + log_message('debug', 'No URI present. Default controller set.'); + } + + // -------------------------------------------------------------------- + + /** + * Validate request + * + * Attempts validate the URI request and determine the controller path. + * + * @used-by CI_Router::_set_request() + * @param array $segments URI segments + * @return mixed URI segments + */ + protected function _validate_request($segments) + { + $c = count($segments); + $directory_override = isset($this->directory); + + // Loop through our segments and return as soon as a controller + // is found or when such a directory doesn't exist + while ($c-- > 0) + { + $test = $this->directory + .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]); + + if ( ! file_exists(APPPATH.'controllers/'.$test.'.php') + && $directory_override === FALSE + && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0]) + ) + { + $this->set_directory(array_shift($segments), TRUE); + continue; + } + + return $segments; + } + + // This means that all segments were actually directories + return $segments; + } + + // -------------------------------------------------------------------- + + /** + * Parse Routes + * + * Matches any routes that may exist in the config/routes.php file + * against the URI to determine if the class/method need to be remapped. + * + * @return void + */ + protected function _parse_routes() + { + // Turn the segment array into a URI string + $uri = implode('/', $this->uri->segments); + + // Get HTTP verb + $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli'; + + // Loop through the route array looking for wildcards + foreach ($this->routes as $key => $val) + { + // Check if route format is using HTTP verbs + if (is_array($val)) + { + $val = array_change_key_case($val, CASE_LOWER); + if (isset($val[$http_verb])) + { + $val = $val[$http_verb]; + } + else + { + continue; + } + } + + // Convert wildcards to RegEx + $key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key); + + // Does the RegEx match? + if (preg_match('#^'.$key.'$#', $uri, $matches)) + { + // Are we using callbacks to process back-references? + if ( ! is_string($val) && is_callable($val)) + { + // Remove the original string from the matches array. + array_shift($matches); + + // Execute the callback using the values in matches as its parameters. + $val = call_user_func_array($val, $matches); + } + // Are we using the default routing method for back-references? + elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE) + { + $val = preg_replace('#^'.$key.'$#', $val, $uri); + } + + $this->_set_request(explode('/', $val)); + return; + } + } + + // If we got this far it means we didn't encounter a + // matching route so we'll set the site default route + $this->_set_request(array_values($this->uri->segments)); + } + + // -------------------------------------------------------------------- + + /** + * Set class name + * + * @param string $class Class name + * @return void + */ + public function set_class($class) + { + $this->class = str_replace(array('/', '.'), '', $class); + } + + // -------------------------------------------------------------------- + + /** + * Fetch the current class + * + * @deprecated 3.0.0 Read the 'class' property instead + * @return string + */ + public function fetch_class() + { + return $this->class; + } + + // -------------------------------------------------------------------- + + /** + * Set method name + * + * @param string $method Method name + * @return void + */ + public function set_method($method) + { + $this->method = $method; + } + + // -------------------------------------------------------------------- + + /** + * Fetch the current method + * + * @deprecated 3.0.0 Read the 'method' property instead + * @return string + */ + public function fetch_method() + { + return $this->method; + } + + // -------------------------------------------------------------------- + + /** + * Set directory name + * + * @param string $dir Directory name + * @param bool $append Whether we're appending rather than setting the full value + * @return void + */ + public function set_directory($dir, $append = FALSE) + { + if ($append !== TRUE OR empty($this->directory)) + { + $this->directory = str_replace('.', '', trim($dir, '/')).'/'; + } + else + { + $this->directory .= str_replace('.', '', trim($dir, '/')).'/'; + } + } + + // -------------------------------------------------------------------- + + /** + * Fetch directory + * + * Feches the sub-directory (if any) that contains the requested + * controller class. + * + * @deprecated 3.0.0 Read the 'directory' property instead + * @return string + */ + public function fetch_directory() + { + return $this->directory; + } + +} diff --git a/system/core/Security.php b/system/core/Security.php new file mode 100644 index 0000000..e7772e0 --- /dev/null +++ b/system/core/Security.php @@ -0,0 +1,1111 @@ +', '<', '>', + "'", '"', '&', '$', '#', + '{', '}', '[', ']', '=', + ';', '?', '%20', '%22', + '%3c', // < + '%253c', // < + '%3e', // > + '%0e', // > + '%28', // ( + '%29', // ) + '%2528', // ( + '%26', // & + '%24', // $ + '%3f', // ? + '%3b', // ; + '%3d' // = + ); + + /** + * Character set + * + * Will be overridden by the constructor. + * + * @var string + */ + public $charset = 'UTF-8'; + + /** + * XSS Hash + * + * Random Hash for protecting URLs. + * + * @var string + */ + protected $_xss_hash; + + /** + * CSRF Hash + * + * Random hash for Cross Site Request Forgery protection cookie + * + * @var string + */ + protected $_csrf_hash; + + /** + * CSRF Expire time + * + * Expiration time for Cross Site Request Forgery protection cookie. + * Defaults to two hours (in seconds). + * + * @var int + */ + protected $_csrf_expire = 7200; + + /** + * CSRF Token name + * + * Token name for Cross Site Request Forgery protection cookie. + * + * @var string + */ + protected $_csrf_token_name = 'ci_csrf_token'; + + /** + * CSRF Cookie name + * + * Cookie name for Cross Site Request Forgery protection cookie. + * + * @var string + */ + protected $_csrf_cookie_name = 'ci_csrf_token'; + + /** + * List of never allowed strings + * + * @var array + */ + protected $_never_allowed_str = array( + 'document.cookie' => '[removed]', + '(document).cookie' => '[removed]', + 'document.write' => '[removed]', + '(document).write' => '[removed]', + '.parentNode' => '[removed]', + '.innerHTML' => '[removed]', + '-moz-binding' => '[removed]', + '' => '-->', + ' '<![CDATA[', + '' => '<comment>', + '<%' => '<%' + ); + + /** + * List of never allowed regex replacements + * + * @var array + */ + protected $_never_allowed_regex = array( + 'javascript\s*:', + '(\(?document\)?|\(?window\)?(\.document)?)\.(location|on\w*)', + 'expression\s*(\(|&\#40;)', // CSS and IE + 'vbscript\s*:', // IE, surprise! + 'wscript\s*:', // IE + 'jscript\s*:', // IE + 'vbs\s*:', // IE + 'Redirect\s+30\d', + "([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?" + ); + + /** + * Class constructor + * + * @return void + */ + public function __construct() + { + // Is CSRF protection enabled? + if (config_item('csrf_protection')) + { + // CSRF config + foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key) + { + if (NULL !== ($val = config_item($key))) + { + $this->{'_'.$key} = $val; + } + } + + // Append application specific cookie prefix + if ($cookie_prefix = config_item('cookie_prefix')) + { + $this->_csrf_cookie_name = $cookie_prefix.$this->_csrf_cookie_name; + } + + // Set the CSRF hash + $this->_csrf_set_hash(); + } + + $this->charset = strtoupper((string) config_item('charset')); + + log_message('info', 'Security Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * CSRF Verify + * + * @return CI_Security + */ + public function csrf_verify() + { + // If it's not a POST request we will set the CSRF cookie + if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') + { + return $this->csrf_set_cookie(); + } + + // Check if URI has been whitelisted from CSRF checks + if ($exclude_uris = config_item('csrf_exclude_uris')) + { + $uri = load_class('URI', 'core'); + foreach ($exclude_uris as $excluded) + { + if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string())) + { + return $this; + } + } + } + + // Check CSRF token validity, but don't error on mismatch just yet - we'll want to regenerate + $valid = isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]) + && is_string($_POST[$this->_csrf_token_name]) && is_string($_COOKIE[$this->_csrf_cookie_name]) + && hash_equals($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]); + + // We kill this since we're done and we don't want to pollute the _POST array + unset($_POST[$this->_csrf_token_name]); + + // Regenerate on every submission? + if (config_item('csrf_regenerate')) + { + // Nothing should last forever + unset($_COOKIE[$this->_csrf_cookie_name]); + $this->_csrf_hash = NULL; + } + + $this->_csrf_set_hash(); + $this->csrf_set_cookie(); + + if ($valid !== TRUE) + { + $this->csrf_show_error(); + } + + log_message('info', 'CSRF token verified'); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * CSRF Set Cookie + * + * @codeCoverageIgnore + * @return CI_Security + */ + public function csrf_set_cookie() + { + $expire = time() + $this->_csrf_expire; + $secure_cookie = (bool) config_item('cookie_secure'); + + if ($secure_cookie && ! is_https()) + { + return FALSE; + } + + if (is_php('7.3')) + { + setcookie( + $this->_csrf_cookie_name, + $this->_csrf_hash, + array( + 'expires' => $expire, + 'path' => config_item('cookie_path'), + 'domain' => config_item('cookie_domain'), + 'secure' => $secure_cookie, + 'httponly' => config_item('cookie_httponly'), + 'samesite' => 'Strict' + ) + ); + } + else + { + $domain = trim(config_item('cookie_domain')); + header('Set-Cookie: '.$this->_csrf_cookie_name.'='.$this->_csrf_hash + .'; Expires='.gmdate('D, d-M-Y H:i:s T', $expire) + .'; Max-Age='.$this->_csrf_expire + .'; Path='.rawurlencode(config_item('cookie_path')) + .($domain === '' ? '' : '; Domain='.$domain) + .($secure_cookie ? '; Secure' : '') + .(config_item('cookie_httponly') ? '; HttpOnly' : '') + .'; SameSite=Strict' + ); + } + + log_message('info', 'CSRF cookie sent'); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Show CSRF Error + * + * @return void + */ + public function csrf_show_error() + { + show_error('The action you have requested is not allowed.', 403); + } + + // -------------------------------------------------------------------- + + /** + * Get CSRF Hash + * + * @see CI_Security::$_csrf_hash + * @return string CSRF hash + */ + public function get_csrf_hash() + { + return $this->_csrf_hash; + } + + // -------------------------------------------------------------------- + + /** + * Get CSRF Token Name + * + * @see CI_Security::$_csrf_token_name + * @return string CSRF token name + */ + public function get_csrf_token_name() + { + return $this->_csrf_token_name; + } + + // -------------------------------------------------------------------- + + /** + * XSS Clean + * + * Sanitizes data so that Cross Site Scripting Hacks can be + * prevented. This method does a fair amount of work but + * it is extremely thorough, designed to prevent even the + * most obscure XSS attempts. Nothing is ever 100% foolproof, + * of course, but I haven't been able to get anything passed + * the filter. + * + * Note: Should only be used to deal with data upon submission. + * It's not something that should be used for general + * runtime processing. + * + * @link http://channel.bitflux.ch/wiki/XSS_Prevention + * Based in part on some code and ideas from Bitflux. + * + * @link http://ha.ckers.org/xss.html + * To help develop this script I used this great list of + * vulnerabilities along with a few other hacks I've + * harvested from examining vulnerabilities in other programs. + * + * @param string|string[] $str Input data + * @param bool $is_image Whether the input is an image + * @return string + */ + public function xss_clean($str, $is_image = FALSE) + { + // Is the string an array? + if (is_array($str)) + { + foreach ($str as $key => &$value) + { + $str[$key] = $this->xss_clean($value); + } + + return $str; + } + + // Remove Invisible Characters + $str = remove_invisible_characters($str); + + /* + * URL Decode + * + * Just in case stuff like this is submitted: + * + * Google + * + * Note: Use rawurldecode() so it does not remove plus signs + */ + if (stripos($str, '%') !== false) + { + do + { + $oldstr = $str; + $str = rawurldecode($str); + $str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str); + } + while ($oldstr !== $str); + unset($oldstr); + } + + /* + * Convert character entities to ASCII + * + * This permits our tests below to work reliably. + * We only convert entities that are within tags since + * these are the ones that will pose security problems. + */ + $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); + $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str); + + // Remove Invisible Characters Again! + $str = remove_invisible_characters($str); + + /* + * Convert all tabs to spaces + * + * This prevents strings like this: ja vascript + * NOTE: we deal with spaces between characters later. + * NOTE: preg_replace was found to be amazingly slow here on + * large blocks of data, so we use str_replace. + */ + $str = str_replace("\t", ' ', $str); + + // Capture converted string for later comparison + $converted_string = $str; + + // Remove Strings that are never allowed + $str = $this->_do_never_allowed($str); + + /* + * Makes PHP tags safe + * + * Note: XML tags are inadvertently replaced too: + * + * '), array('<?', '?>'), $str); + } + + /* + * Compact any exploded words + * + * This corrects words like: j a v a s c r i p t + * These words are compacted back to their correct state. + */ + $words = array( + 'javascript', 'expression', 'vbscript', 'jscript', 'wscript', + 'vbs', 'script', 'base64', 'applet', 'alert', 'document', + 'write', 'cookie', 'window', 'confirm', 'prompt', 'eval' + ); + + foreach ($words as $word) + { + $word = implode('\s*', str_split($word)).'\s*'; + + // We only want to do this when it is followed by a non-word character + // That way valid stuff like "dealer to" does not become "dealerto" + $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); + } + + /* + * Remove disallowed Javascript in links or img tags + * We used to do some version comparisons and use of stripos(), + * but it is dog slow compared to these simplified non-capturing + * preg_match(), especially if the pattern exists in the string + * + * Note: It was reported that not only space characters, but all in + * the following pattern can be parsed as separators between a tag name + * and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C] + * ... however, remove_invisible_characters() above already strips the + * hex-encoded ones, so we'll skip them below. + */ + do + { + $original = $str; + + if (preg_match('/]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str); + } + + if (preg_match('/]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str); + } + + if (preg_match('/script|xss/i', $str)) + { + $str = preg_replace('##si', '[removed]', $str); + } + } + while ($original !== $str); + unset($original); + + /* + * Sanitize naughty HTML elements + * + * If a tag containing any of the words in the list + * below is found, the tag gets converted to entities. + * + * So this: + * Becomes: <blink> + */ + $pattern = '#' + .'<((?/*\s*)((?[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character + .'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator + // optional attributes + .'(?(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons + .'[^\s\042\047>/=]+' // attribute characters + // optional attribute-value + .'(?:\s*=' // attribute-value separator + .'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value + .')?' // end optional attribute-value group + .')*)' // end optional attributes group + .'[^>]*)(?\>)?#isS'; + + // Note: It would be nice to optimize this for speed, BUT + // only matching the naughty elements here results in + // false positives and in turn - vulnerabilities! + do + { + $old_str = $str; + $str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str); + } + while ($old_str !== $str); + unset($old_str); + + /* + * Sanitize naughty scripting elements + * + * Similar to above, only instead of looking for + * tags it looks for PHP and JavaScript commands + * that are disallowed. Rather than removing the + * code, it simply converts the parenthesis to entities + * rendering the code un-executable. + * + * For example: eval('some code') + * Becomes: eval('some code') + */ + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', + '\\1\\2(\\3)', + $str + ); + + // Same thing, but for "tag functions" (e.g. eval`some code`) + // See https://github.com/bcit-ci/CodeIgniter/issues/5420 + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si', + '\\1\\2`\\3`', + $str + ); + + // Final clean up + // This adds a bit of extra precaution in case + // something got through the above filters + $str = $this->_do_never_allowed($str); + + /* + * Images are Handled in a Special Way + * - Essentially, we want to know that after all of the character + * conversion is done whether any unwanted, likely XSS, code was found. + * If not, we return TRUE, as the image is clean. + * However, if the string post-conversion does not matched the + * string post-removal of XSS, then it fails, as there was unwanted XSS + * code found and removed/changed during processing. + */ + if ($is_image === TRUE) + { + return ($str === $converted_string); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * XSS Hash + * + * Generates the XSS hash if needed and returns it. + * + * @see CI_Security::$_xss_hash + * @return string XSS hash + */ + public function xss_hash() + { + if ($this->_xss_hash === NULL) + { + $rand = $this->get_random_bytes(16); + $this->_xss_hash = ($rand === FALSE) + ? md5(uniqid(mt_rand(), TRUE)) + : bin2hex($rand); + } + + return $this->_xss_hash; + } + + // -------------------------------------------------------------------- + + /** + * Get random bytes + * + * @param int $length Output length + * @return string + */ + public function get_random_bytes($length) + { + if (empty($length) OR ! ctype_digit((string) $length)) + { + return FALSE; + } + + if (function_exists('random_bytes')) + { + try + { + // The cast is required to avoid TypeError + return random_bytes((int) $length); + } + catch (Exception $e) + { + // If random_bytes() can't do the job, we can't either ... + // There's no point in using fallbacks. + log_message('error', $e->getMessage()); + return FALSE; + } + } + + // Unfortunately, none of the following PRNGs is guaranteed to exist ... + if (defined('MCRYPT_DEV_URANDOM') && ($output = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== FALSE) + { + return $output; + } + + if (is_readable('/dev/urandom') && ($fp = fopen('/dev/urandom', 'rb')) !== FALSE) + { + // Try not to waste entropy ... + is_php('5.4') && stream_set_chunk_size($fp, $length); + $output = fread($fp, $length); + fclose($fp); + if ($output !== FALSE) + { + return $output; + } + } + + if (function_exists('openssl_random_pseudo_bytes')) + { + return openssl_random_pseudo_bytes($length); + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * HTML Entities Decode + * + * A replacement for html_entity_decode() + * + * The reason we are not using html_entity_decode() by itself is because + * while it is not technically correct to leave out the semicolon + * at the end of an entity most browsers will still interpret the entity + * correctly. html_entity_decode() does not convert entities without + * semicolons, so we are left with our own little solution here. Bummer. + * + * @link http://php.net/html-entity-decode + * + * @param string $str Input + * @param string $charset Character set + * @return string + */ + public function entity_decode($str, $charset = NULL) + { + if (strpos($str, '&') === FALSE) + { + return $str; + } + + static $_entities; + + isset($charset) OR $charset = $this->charset; + $flag = is_php('5.4') + ? ENT_COMPAT | ENT_HTML5 + : ENT_COMPAT; + + if ( ! isset($_entities)) + { + $_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, $flag, $charset)); + + // If we're not on PHP 5.4+, add the possibly dangerous HTML 5 + // entities to the array manually + if ($flag === ENT_COMPAT) + { + $_entities[':'] = ':'; + $_entities['('] = '('; + $_entities[')'] = ')'; + $_entities["\n"] = ' '; + $_entities["\t"] = ' '; + } + } + + do + { + $str_compare = $str; + + // Decode standard entities, avoiding false positives + if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches)) + { + $replace = array(); + $matches = array_unique(array_map('strtolower', $matches[0])); + foreach ($matches as &$match) + { + if (($char = array_search($match.';', $_entities, TRUE)) !== FALSE) + { + $replace[$match] = $char; + } + } + + $str = str_replace(array_keys($replace), array_values($replace), $str); + } + + // Decode numeric & UTF16 two byte entities + $str = html_entity_decode( + preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str), + $flag, + $charset + ); + + if ($flag === ENT_COMPAT) + { + $str = str_replace(array_values($_entities), array_keys($_entities), $str); + } + } + while ($str_compare !== $str); + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Sanitize Filename + * + * @param string $str Input file name + * @param bool $relative_path Whether to preserve paths + * @return string + */ + public function sanitize_filename($str, $relative_path = FALSE) + { + $bad = $this->filename_bad_chars; + + if ( ! $relative_path) + { + $bad[] = './'; + $bad[] = '/'; + } + + $str = remove_invisible_characters($str, FALSE); + + do + { + $old = $str; + $str = str_replace($bad, '', $str); + } + while ($old !== $str); + + return stripslashes($str); + } + + // ---------------------------------------------------------------- + + /** + * Strip Image Tags + * + * @param string $str + * @return string + */ + public function strip_image_tags($str) + { + return preg_replace( + array( + '##i', + '#`]+)).*?\>#i' + ), + '\\2', + $str + ); + } + + // ---------------------------------------------------------------- + + /** + * URL-decode taking spaces into account + * + * @see https://github.com/bcit-ci/CodeIgniter/issues/4877 + * @param array $matches + * @return string + */ + protected function _urldecodespaces($matches) + { + $input = $matches[0]; + $nospaces = preg_replace('#\s+#', '', $input); + return ($nospaces === $input) + ? $input + : rawurldecode($nospaces); + } + + // ---------------------------------------------------------------- + + /** + * Compact Exploded Words + * + * Callback method for xss_clean() to remove whitespace from + * things like 'j a v a s c r i p t'. + * + * @used-by CI_Security::xss_clean() + * @param array $matches + * @return string + */ + protected function _compact_exploded_words($matches) + { + return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; + } + + // -------------------------------------------------------------------- + + /** + * Sanitize Naughty HTML + * + * Callback method for xss_clean() to remove naughty HTML elements. + * + * @used-by CI_Security::xss_clean() + * @param array $matches + * @return string + */ + protected function _sanitize_naughty_html($matches) + { + static $naughty_tags = array( + 'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound', + 'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer', + 'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object', + 'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss' + ); + + static $evil_attributes = array( + 'on\w+', 'style', 'xmlns', 'formaction', 'form', 'xlink:href', 'FSCommand', 'seekSegmentTime' + ); + + // First, escape unclosed tags + if (empty($matches['closeTag'])) + { + return '<'.$matches[1]; + } + // Is the element that we caught naughty? If so, escape it + elseif (in_array(strtolower($matches['tagName']), $naughty_tags, TRUE)) + { + return '<'.$matches[1].'>'; + } + // For other tags, see if their attributes are "evil" and strip those + elseif (isset($matches['attributes'])) + { + // We'll store the already filtered attributes here + $attributes = array(); + + // Attribute-catching pattern + $attributes_pattern = '#' + .'(?[^\s\042\047>/=]+)' // attribute characters + // optional attribute-value + .'(?:\s*=(?[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator + .'#i'; + + // Blacklist pattern for evil attribute names + $is_evil_pattern = '#^('.implode('|', $evil_attributes).')$#i'; + + // Each iteration filters a single attribute + do + { + // Strip any non-alpha characters that may precede an attribute. + // Browsers often parse these incorrectly and that has been a + // of numerous XSS issues we've had. + $matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']); + + if ( ! preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE)) + { + // No (valid) attribute found? Discard everything else inside the tag + break; + } + + if ( + // Is it indeed an "evil" attribute? + preg_match($is_evil_pattern, $attribute['name'][0]) + // Or does it have an equals sign, but no value and not quoted? Strip that too! + OR (trim($attribute['value'][0]) === '') + ) + { + $attributes[] = 'xss=removed'; + } + else + { + $attributes[] = $attribute[0][0]; + } + + $matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0])); + } + while ($matches['attributes'] !== ''); + + $attributes = empty($attributes) + ? '' + : ' '.implode(' ', $attributes); + return '<'.$matches['slash'].$matches['tagName'].$attributes.'>'; + } + + return $matches[0]; + } + + // -------------------------------------------------------------------- + + /** + * JS Link Removal + * + * Callback method for xss_clean() to sanitize links. + * + * This limits the PCRE backtracks, making it more performance friendly + * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in + * PHP 5.2+ on link-heavy strings. + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _js_link_removal($match) + { + return str_replace( + $match[1], + preg_replace( + '#href=.*?(?:(?:alert|prompt|confirm)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); + } + + // -------------------------------------------------------------------- + + /** + * JS Image Removal + * + * Callback method for xss_clean() to sanitize image tags. + * + * This limits the PCRE backtracks, making it more performance friendly + * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in + * PHP 5.2+ on image tag heavy strings. + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _js_img_removal($match) + { + return str_replace( + $match[1], + preg_replace( + '#src=.*?(?:(?:alert|prompt|confirm|eval)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); + } + + // -------------------------------------------------------------------- + + /** + * Attribute Conversion + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _convert_attribute($match) + { + return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]); + } + + // -------------------------------------------------------------------- + + /** + * Filter Attributes + * + * Filters tag attributes for consistency and safety. + * + * @used-by CI_Security::_js_img_removal() + * @used-by CI_Security::_js_link_removal() + * @param string $str + * @return string + */ + protected function _filter_attributes($str) + { + $out = ''; + if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) + { + foreach ($matches[0] as $match) + { + $out .= preg_replace('#/\*.*?\*/#s', '', $match); + } + } + + return $out; + } + + // -------------------------------------------------------------------- + + /** + * HTML Entity Decode Callback + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _decode_entity($match) + { + // Protect GET variables in URLs + // 901119URL5918AMP18930PROTECT8198 + $match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash().'\\1=\\2', $match[0]); + + // Decode, then un-protect URL GET vars + return str_replace( + $this->xss_hash(), + '&', + $this->entity_decode($match, $this->charset) + ); + } + + // -------------------------------------------------------------------- + + /** + * Do Never Allowed + * + * @used-by CI_Security::xss_clean() + * @param string + * @return string + */ + protected function _do_never_allowed($str) + { + $str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str); + + foreach ($this->_never_allowed_regex as $regex) + { + $str = preg_replace('#'.$regex.'#is', '[removed]', $str); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Set CSRF Hash and Cookie + * + * @return string + */ + protected function _csrf_set_hash() + { + if ($this->_csrf_hash === NULL) + { + // If the cookie exists we will use its value. + // We don't necessarily want to regenerate it with + // each page load since a page could contain embedded + // sub-pages causing this feature to fail + if (isset($_COOKIE[$this->_csrf_cookie_name]) && is_string($_COOKIE[$this->_csrf_cookie_name]) + && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->_csrf_cookie_name]) === 1) + { + return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name]; + } + + $rand = $this->get_random_bytes(16); + $this->_csrf_hash = ($rand === FALSE) + ? md5(uniqid(mt_rand(), TRUE)) + : bin2hex($rand); + } + + return $this->_csrf_hash; + } + +} diff --git a/system/core/URI.php b/system/core/URI.php new file mode 100644 index 0000000..6a55439 --- /dev/null +++ b/system/core/URI.php @@ -0,0 +1,644 @@ +config =& load_class('Config', 'core'); + + // If query strings are enabled, we don't need to parse any segments. + // However, they don't make sense under CLI. + if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE) + { + $this->_permitted_uri_chars = $this->config->item('permitted_uri_chars'); + + // If it's a CLI request, ignore the configuration + if (is_cli()) + { + $uri = $this->_parse_argv(); + } + else + { + $protocol = $this->config->item('uri_protocol'); + empty($protocol) && $protocol = 'REQUEST_URI'; + + switch ($protocol) + { + case 'AUTO': // For BC purposes only + case 'REQUEST_URI': + $uri = $this->_parse_request_uri(); + break; + case 'QUERY_STRING': + $uri = $this->_parse_query_string(); + break; + case 'PATH_INFO': + default: + $uri = isset($_SERVER[$protocol]) + ? $_SERVER[$protocol] + : $this->_parse_request_uri(); + break; + } + } + + $this->_set_uri_string($uri); + } + + log_message('info', 'URI Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Set URI String + * + * @param string $str + * @return void + */ + protected function _set_uri_string($str) + { + // Filter out control characters and trim slashes + $this->uri_string = trim(remove_invisible_characters($str, FALSE), '/'); + + if ($this->uri_string !== '') + { + // Remove the URL suffix, if present + if (($suffix = (string) $this->config->item('url_suffix')) !== '') + { + $slen = strlen($suffix); + + if (substr($this->uri_string, -$slen) === $suffix) + { + $this->uri_string = substr($this->uri_string, 0, -$slen); + } + } + + $this->segments[0] = NULL; + // Populate the segments array + foreach (explode('/', trim($this->uri_string, '/')) as $val) + { + $val = trim($val); + // Filter segments for security + $this->filter_uri($val); + + if ($val !== '') + { + $this->segments[] = $val; + } + } + + unset($this->segments[0]); + } + } + + // -------------------------------------------------------------------- + + /** + * Parse REQUEST_URI + * + * Will parse REQUEST_URI and automatically detect the URI from it, + * while fixing the query string if necessary. + * + * @return string + */ + protected function _parse_request_uri() + { + if ( ! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) + { + return ''; + } + + // parse_url() returns false if no host is present, but the path or query string + // contains a colon followed by a number + $uri = parse_url('http://dummy'.$_SERVER['REQUEST_URI']); + $query = isset($uri['query']) ? $uri['query'] : ''; + $uri = isset($uri['path']) ? $uri['path'] : ''; + + if (isset($_SERVER['SCRIPT_NAME'][0])) + { + if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) + { + $uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME'])); + } + elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0) + { + $uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME']))); + } + } + + // This section ensures that even on servers that require the URI to be in the query string (Nginx) a correct + // URI is found, and also fixes the QUERY_STRING server var and $_GET array. + if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0) + { + $query = explode('?', $query, 2); + $uri = $query[0]; + $_SERVER['QUERY_STRING'] = isset($query[1]) ? $query[1] : ''; + } + else + { + $_SERVER['QUERY_STRING'] = $query; + } + + parse_str($_SERVER['QUERY_STRING'], $_GET); + + if ($uri === '/' OR $uri === '') + { + return '/'; + } + + // Do some final cleaning of the URI and return it + return $this->_remove_relative_directory($uri); + } + + // -------------------------------------------------------------------- + + /** + * Parse QUERY_STRING + * + * Will parse QUERY_STRING and automatically detect the URI from it. + * + * @return string + */ + protected function _parse_query_string() + { + $uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING'); + + if (trim($uri, '/') === '') + { + return ''; + } + elseif (strncmp($uri, '/', 1) === 0) + { + $uri = explode('?', $uri, 2); + $_SERVER['QUERY_STRING'] = isset($uri[1]) ? $uri[1] : ''; + $uri = $uri[0]; + } + + parse_str($_SERVER['QUERY_STRING'], $_GET); + + return $this->_remove_relative_directory($uri); + } + + // -------------------------------------------------------------------- + + /** + * Parse CLI arguments + * + * Take each command line argument and assume it is a URI segment. + * + * @return string + */ + protected function _parse_argv() + { + $args = array_slice($_SERVER['argv'], 1); + return $args ? implode('/', $args) : ''; + } + + // -------------------------------------------------------------------- + + /** + * Remove relative directory (../) and multi slashes (///) + * + * Do some final cleaning of the URI and return it, currently only used in self::_parse_request_uri() + * + * @param string $uri + * @return string + */ + protected function _remove_relative_directory($uri) + { + $uris = array(); + $tok = strtok($uri, '/'); + while ($tok !== FALSE) + { + if (( ! empty($tok) OR $tok === '0') && $tok !== '..') + { + $uris[] = $tok; + } + $tok = strtok('/'); + } + + return implode('/', $uris); + } + + // -------------------------------------------------------------------- + + /** + * Filter URI + * + * Filters segments for malicious characters. + * + * @param string $str + * @return void + */ + public function filter_uri(&$str) + { + if ( ! empty($str) && ! empty($this->_permitted_uri_chars) && ! preg_match('/^['.$this->_permitted_uri_chars.']+$/i'.(UTF8_ENABLED ? 'u' : ''), $str)) + { + show_error('The URI you submitted has disallowed characters.', 400); + } + } + + // -------------------------------------------------------------------- + + /** + * Fetch URI Segment + * + * @see CI_URI::$segments + * @param int $n Index + * @param mixed $no_result What to return if the segment index is not found + * @return mixed + */ + public function segment($n, $no_result = NULL) + { + return isset($this->segments[$n]) ? $this->segments[$n] : $no_result; + } + + // -------------------------------------------------------------------- + + /** + * Fetch URI "routed" Segment + * + * Returns the re-routed URI segment (assuming routing rules are used) + * based on the index provided. If there is no routing, will return + * the same result as CI_URI::segment(). + * + * @see CI_URI::$rsegments + * @see CI_URI::segment() + * @param int $n Index + * @param mixed $no_result What to return if the segment index is not found + * @return mixed + */ + public function rsegment($n, $no_result = NULL) + { + return isset($this->rsegments[$n]) ? $this->rsegments[$n] : $no_result; + } + + // -------------------------------------------------------------------- + + /** + * URI to assoc + * + * Generates an associative array of URI data starting at the supplied + * segment index. For example, if this is your URI: + * + * example.com/user/search/name/joe/location/UK/gender/male + * + * You can use this method to generate an array with this prototype: + * + * array ( + * name => joe + * location => UK + * gender => male + * ) + * + * @param int $n Index (default: 3) + * @param array $default Default values + * @return array + */ + public function uri_to_assoc($n = 3, $default = array()) + { + return $this->_uri_to_assoc($n, $default, 'segment'); + } + + // -------------------------------------------------------------------- + + /** + * Routed URI to assoc + * + * Identical to CI_URI::uri_to_assoc(), only it uses the re-routed + * segment array. + * + * @see CI_URI::uri_to_assoc() + * @param int $n Index (default: 3) + * @param array $default Default values + * @return array + */ + public function ruri_to_assoc($n = 3, $default = array()) + { + return $this->_uri_to_assoc($n, $default, 'rsegment'); + } + + // -------------------------------------------------------------------- + + /** + * Internal URI-to-assoc + * + * Generates a key/value pair from the URI string or re-routed URI string. + * + * @used-by CI_URI::uri_to_assoc() + * @used-by CI_URI::ruri_to_assoc() + * @param int $n Index (default: 3) + * @param array $default Default values + * @param string $which Array name ('segment' or 'rsegment') + * @return array + */ + protected function _uri_to_assoc($n = 3, $default = array(), $which = 'segment') + { + if ( ! is_numeric($n)) + { + return $default; + } + + if (isset($this->keyval[$which], $this->keyval[$which][$n])) + { + return $this->keyval[$which][$n]; + } + + $total_segments = "total_{$which}s"; + $segment_array = "{$which}_array"; + + if ($this->$total_segments() < $n) + { + return (count($default) === 0) + ? array() + : array_fill_keys($default, NULL); + } + + $segments = array_slice($this->$segment_array(), ($n - 1)); + $i = 0; + $lastval = ''; + $retval = array(); + foreach ($segments as $seg) + { + if ($i % 2) + { + $retval[$lastval] = $seg; + } + else + { + $retval[$seg] = NULL; + $lastval = $seg; + } + + $i++; + } + + if (count($default) > 0) + { + foreach ($default as $val) + { + if ( ! array_key_exists($val, $retval)) + { + $retval[$val] = NULL; + } + } + } + + // Cache the array for reuse + isset($this->keyval[$which]) OR $this->keyval[$which] = array(); + $this->keyval[$which][$n] = $retval; + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Assoc to URI + * + * Generates a URI string from an associative array. + * + * @param array $array Input array of key/value pairs + * @return string URI string + */ + public function assoc_to_uri($array) + { + $temp = array(); + foreach ((array) $array as $key => $val) + { + $temp[] = $key; + $temp[] = $val; + } + + return implode('/', $temp); + } + + // -------------------------------------------------------------------- + + /** + * Slash segment + * + * Fetches an URI segment with a slash. + * + * @param int $n Index + * @param string $where Where to add the slash ('trailing' or 'leading') + * @return string + */ + public function slash_segment($n, $where = 'trailing') + { + return $this->_slash_segment($n, $where, 'segment'); + } + + // -------------------------------------------------------------------- + + /** + * Slash routed segment + * + * Fetches an URI routed segment with a slash. + * + * @param int $n Index + * @param string $where Where to add the slash ('trailing' or 'leading') + * @return string + */ + public function slash_rsegment($n, $where = 'trailing') + { + return $this->_slash_segment($n, $where, 'rsegment'); + } + + // -------------------------------------------------------------------- + + /** + * Internal Slash segment + * + * Fetches an URI Segment and adds a slash to it. + * + * @used-by CI_URI::slash_segment() + * @used-by CI_URI::slash_rsegment() + * + * @param int $n Index + * @param string $where Where to add the slash ('trailing' or 'leading') + * @param string $which Array name ('segment' or 'rsegment') + * @return string + */ + protected function _slash_segment($n, $where = 'trailing', $which = 'segment') + { + $leading = $trailing = '/'; + + if ($where === 'trailing') + { + $leading = ''; + } + elseif ($where === 'leading') + { + $trailing = ''; + } + + return $leading.$this->$which($n).$trailing; + } + + // -------------------------------------------------------------------- + + /** + * Segment Array + * + * @return array CI_URI::$segments + */ + public function segment_array() + { + return $this->segments; + } + + // -------------------------------------------------------------------- + + /** + * Routed Segment Array + * + * @return array CI_URI::$rsegments + */ + public function rsegment_array() + { + return $this->rsegments; + } + + // -------------------------------------------------------------------- + + /** + * Total number of segments + * + * @return int + */ + public function total_segments() + { + return count($this->segments); + } + + // -------------------------------------------------------------------- + + /** + * Total number of routed segments + * + * @return int + */ + public function total_rsegments() + { + return count($this->rsegments); + } + + // -------------------------------------------------------------------- + + /** + * Fetch URI string + * + * @return string CI_URI::$uri_string + */ + public function uri_string() + { + return $this->uri_string; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Re-routed URI string + * + * @return string + */ + public function ruri_string() + { + return ltrim(load_class('Router', 'core')->directory, '/').implode('/', $this->rsegments); + } + +} diff --git a/system/core/Utf8.php b/system/core/Utf8.php new file mode 100644 index 0000000..0547223 --- /dev/null +++ b/system/core/Utf8.php @@ -0,0 +1,165 @@ +is_ascii($str) === FALSE) + { + if (MB_ENABLED) + { + $str = mb_convert_encoding($str, 'UTF-8', 'UTF-8'); + } + elseif (ICONV_ENABLED) + { + $str = @iconv('UTF-8', 'UTF-8//IGNORE', $str); + } + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Remove ASCII control characters + * + * Removes all ASCII control characters except horizontal tabs, + * line feeds, and carriage returns, as all others can cause + * problems in XML. + * + * @param string $str String to clean + * @return string + */ + public function safe_ascii_for_xml($str) + { + return remove_invisible_characters($str, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Convert to UTF-8 + * + * Attempts to convert a string to UTF-8. + * + * @param string $str Input string + * @param string $encoding Input encoding + * @return string $str encoded in UTF-8 or FALSE on failure + */ + public function convert_to_utf8($str, $encoding) + { + if (MB_ENABLED) + { + return mb_convert_encoding($str, 'UTF-8', $encoding); + } + elseif (ICONV_ENABLED) + { + return @iconv($encoding, 'UTF-8', $str); + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Is ASCII? + * + * Tests if a string is standard 7-bit ASCII or not. + * + * @param string $str String to check + * @return bool + */ + public function is_ascii($str) + { + return (preg_match('/[^\x00-\x7F]/S', $str) === 0); + } + +} diff --git a/system/core/compat/hash.php b/system/core/compat/hash.php new file mode 100644 index 0000000..3fe3b85 --- /dev/null +++ b/system/core/compat/hash.php @@ -0,0 +1,255 @@ + 32, + 'haval128,3' => 128, + 'haval160,3' => 128, + 'haval192,3' => 128, + 'haval224,3' => 128, + 'haval256,3' => 128, + 'haval128,4' => 128, + 'haval160,4' => 128, + 'haval192,4' => 128, + 'haval224,4' => 128, + 'haval256,4' => 128, + 'haval128,5' => 128, + 'haval160,5' => 128, + 'haval192,5' => 128, + 'haval224,5' => 128, + 'haval256,5' => 128, + 'md2' => 16, + 'md4' => 64, + 'md5' => 64, + 'ripemd128' => 64, + 'ripemd160' => 64, + 'ripemd256' => 64, + 'ripemd320' => 64, + 'salsa10' => 64, + 'salsa20' => 64, + 'sha1' => 64, + 'sha224' => 64, + 'sha256' => 64, + 'sha384' => 128, + 'sha512' => 128, + 'snefru' => 32, + 'snefru256' => 32, + 'tiger128,3' => 64, + 'tiger160,3' => 64, + 'tiger192,3' => 64, + 'tiger128,4' => 64, + 'tiger160,4' => 64, + 'tiger192,4' => 64, + 'whirlpool' => 64 + ); + + if (isset($block_sizes[$algo], $password[$block_sizes[$algo]])) + { + $password = hash($algo, $password, TRUE); + } + + $hash = ''; + // Note: Blocks are NOT 0-indexed + for ($bc = (int) ceil($length / $hash_length), $bi = 1; $bi <= $bc; $bi++) + { + $key = $derived_key = hash_hmac($algo, $salt.pack('N', $bi), $password, TRUE); + for ($i = 1; $i < $iterations; $i++) + { + $derived_key ^= $key = hash_hmac($algo, $key, $password, TRUE); + } + + $hash .= $derived_key; + } + + // This is not RFC-compatible, but we're aiming for natural PHP compatibility + if ( ! $raw_output) + { + $hash = bin2hex($hash); + } + + return defined('MB_OVERLOAD_STRING') + ? mb_substr($hash, 0, $length, '8bit') + : substr($hash, 0, $length); + } +} diff --git a/system/core/compat/index.html b/system/core/compat/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/core/compat/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/core/compat/mbstring.php b/system/core/compat/mbstring.php new file mode 100644 index 0000000..1c49d18 --- /dev/null +++ b/system/core/compat/mbstring.php @@ -0,0 +1,150 @@ + 0, 'algoName' => 'unknown', 'options' => array()) + : array('algo' => 1, 'algoName' => 'bcrypt', 'options' => array('cost' => $hash)); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('password_hash')) +{ + /** + * password_hash() + * + * @link http://php.net/password_hash + * @param string $password + * @param int $algo + * @param array $options + * @return mixed + */ + function password_hash($password, $algo, array $options = array()) + { + static $func_overload; + isset($func_overload) OR $func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); + + if ($algo !== 1) + { + trigger_error('password_hash(): Unknown hashing algorithm: '.(int) $algo, E_USER_WARNING); + return NULL; + } + + if (isset($options['cost']) && ($options['cost'] < 4 OR $options['cost'] > 31)) + { + trigger_error('password_hash(): Invalid bcrypt cost parameter specified: '.(int) $options['cost'], E_USER_WARNING); + return NULL; + } + + if (isset($options['salt']) && ($saltlen = ($func_overload ? mb_strlen($options['salt'], '8bit') : strlen($options['salt']))) < 22) + { + trigger_error('password_hash(): Provided salt is too short: '.$saltlen.' expecting 22', E_USER_WARNING); + return NULL; + } + elseif ( ! isset($options['salt'])) + { + if (function_exists('random_bytes')) + { + try + { + $options['salt'] = random_bytes(16); + } + catch (Exception $e) + { + log_message('error', 'compat/password: Error while trying to use random_bytes(): '.$e->getMessage()); + return FALSE; + } + } + elseif (defined('MCRYPT_DEV_URANDOM')) + { + $options['salt'] = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM); + } + elseif (DIRECTORY_SEPARATOR === '/' && (is_readable($dev = '/dev/arandom') OR is_readable($dev = '/dev/urandom'))) + { + if (($fp = fopen($dev, 'rb')) === FALSE) + { + log_message('error', 'compat/password: Unable to open '.$dev.' for reading.'); + return FALSE; + } + + // Try not to waste entropy ... + is_php('5.4') && stream_set_chunk_size($fp, 16); + + $options['salt'] = ''; + for ($read = 0; $read < 16; $read = ($func_overload) ? mb_strlen($options['salt'], '8bit') : strlen($options['salt'])) + { + if (($read = fread($fp, 16 - $read)) === FALSE) + { + log_message('error', 'compat/password: Error while reading from '.$dev.'.'); + return FALSE; + } + $options['salt'] .= $read; + } + + fclose($fp); + } + elseif (function_exists('openssl_random_pseudo_bytes')) + { + $is_secure = NULL; + $options['salt'] = openssl_random_pseudo_bytes(16, $is_secure); + if ($is_secure !== TRUE) + { + log_message('error', 'compat/password: openssl_random_pseudo_bytes() set the $cryto_strong flag to FALSE'); + return FALSE; + } + } + else + { + log_message('error', 'compat/password: No CSPRNG available.'); + return FALSE; + } + + $options['salt'] = str_replace('+', '.', rtrim(base64_encode($options['salt']), '=')); + } + elseif ( ! preg_match('#^[a-zA-Z0-9./]+$#D', $options['salt'])) + { + $options['salt'] = str_replace('+', '.', rtrim(base64_encode($options['salt']), '=')); + } + + isset($options['cost']) OR $options['cost'] = 10; + + return (strlen($password = crypt($password, sprintf('$2y$%02d$%s', $options['cost'], $options['salt']))) === 60) + ? $password + : FALSE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('password_needs_rehash')) +{ + /** + * password_needs_rehash() + * + * @link http://php.net/password_needs_rehash + * @param string $hash + * @param int $algo + * @param array $options + * @return bool + */ + function password_needs_rehash($hash, $algo, array $options = array()) + { + $info = password_get_info($hash); + + if ($algo !== $info['algo']) + { + return TRUE; + } + elseif ($algo === 1) + { + $options['cost'] = isset($options['cost']) ? (int) $options['cost'] : 10; + return ($info['options']['cost'] !== $options['cost']); + } + + // Odd at first glance, but according to a comment in PHP's own unit tests, + // because it is an unknown algorithm - it's valid and therefore doesn't + // need rehashing. + return FALSE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('password_verify')) +{ + /** + * password_verify() + * + * @link http://php.net/password_verify + * @param string $password + * @param string $hash + * @return bool + */ + function password_verify($password, $hash) + { + if (strlen($hash) !== 60 OR strlen($password = crypt($password, $hash)) !== 60) + { + return FALSE; + } + + $compare = 0; + for ($i = 0; $i < 60; $i++) + { + $compare |= (ord($password[$i]) ^ ord($hash[$i])); + } + + return ($compare === 0); + } +} diff --git a/system/core/compat/standard.php b/system/core/compat/standard.php new file mode 100644 index 0000000..18b1281 --- /dev/null +++ b/system/core/compat/standard.php @@ -0,0 +1,183 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/DB.php b/system/database/DB.php new file mode 100644 index 0000000..23581af --- /dev/null +++ b/system/database/DB.php @@ -0,0 +1,219 @@ +load->get_package_paths() as $path) + { + if ($path !== APPPATH) + { + if (file_exists($file_path = $path.'config/'.ENVIRONMENT.'/database.php')) + { + include($file_path); + } + elseif (file_exists($file_path = $path.'config/database.php')) + { + include($file_path); + } + } + } + } + + if ( ! isset($db) OR count($db) === 0) + { + show_error('No database connection settings were found in the database config file.'); + } + + if ($params !== '') + { + $active_group = $params; + } + + if ( ! isset($active_group)) + { + show_error('You have not specified a database connection group via $active_group in your config/database.php file.'); + } + elseif ( ! isset($db[$active_group])) + { + show_error('You have specified an invalid database connection group ('.$active_group.') in your config/database.php file.'); + } + + $params = $db[$active_group]; + } + elseif (is_string($params)) + { + /** + * Parse the URL from the DSN string + * Database settings can be passed as discreet + * parameters or as a data source name in the first + * parameter. DSNs must have this prototype: + * $dsn = 'driver://username:password@hostname/database'; + */ + if (($dsn = @parse_url($params)) === FALSE) + { + show_error('Invalid DB Connection String'); + } + + $params = array( + 'dbdriver' => $dsn['scheme'], + 'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '', + 'port' => isset($dsn['port']) ? rawurldecode($dsn['port']) : '', + 'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '', + 'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '', + 'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : '' + ); + + // Were additional config items set? + if (isset($dsn['query'])) + { + parse_str($dsn['query'], $extra); + + foreach ($extra as $key => $val) + { + if (is_string($val) && in_array(strtoupper($val), array('TRUE', 'FALSE', 'NULL'))) + { + $val = var_export($val, TRUE); + } + + $params[$key] = $val; + } + } + } + + // No DB specified yet? Beat them senseless... + if (empty($params['dbdriver'])) + { + show_error('You have not selected a database type to connect to.'); + } + + // Load the DB classes. Note: Since the query builder class is optional + // we need to dynamically create a class that extends proper parent class + // based on whether we're using the query builder class or not. + if ($query_builder_override !== NULL) + { + $query_builder = $query_builder_override; + } + // Backwards compatibility work-around for keeping the + // $active_record config variable working. Should be + // removed in v3.1 + elseif ( ! isset($query_builder) && isset($active_record)) + { + $query_builder = $active_record; + } + + require_once(BASEPATH.'database/DB_driver.php'); + + if ( ! isset($query_builder) OR $query_builder === TRUE) + { + require_once(BASEPATH.'database/DB_query_builder.php'); + if ( ! class_exists('CI_DB', FALSE)) + { + /** + * CI_DB + * + * Acts as an alias for both CI_DB_driver and CI_DB_query_builder. + * + * @see CI_DB_query_builder + * @see CI_DB_driver + */ + class CI_DB extends CI_DB_query_builder { } + } + } + elseif ( ! class_exists('CI_DB', FALSE)) + { + /** + * @ignore + */ + class CI_DB extends CI_DB_driver { } + } + + // Load the DB driver + $driver_file = BASEPATH.'database/drivers/'.$params['dbdriver'].'/'.$params['dbdriver'].'_driver.php'; + + file_exists($driver_file) OR show_error('Invalid DB driver'); + require_once($driver_file); + + // Instantiate the DB adapter + $driver = 'CI_DB_'.$params['dbdriver'].'_driver'; + $DB = new $driver($params); + + // Check for a subdriver + if ( ! empty($DB->subdriver)) + { + $driver_file = BASEPATH.'database/drivers/'.$DB->dbdriver.'/subdrivers/'.$DB->dbdriver.'_'.$DB->subdriver.'_driver.php'; + + if (file_exists($driver_file)) + { + require_once($driver_file); + $driver = 'CI_DB_'.$DB->dbdriver.'_'.$DB->subdriver.'_driver'; + $DB = new $driver($params); + } + } + + $DB->initialize(); + return $DB; +} diff --git a/system/database/DB_cache.php b/system/database/DB_cache.php new file mode 100644 index 0000000..d05ebb2 --- /dev/null +++ b/system/database/DB_cache.php @@ -0,0 +1,222 @@ +CI and load the file helper since we use it a lot + $this->CI =& get_instance(); + $this->db =& $db; + $this->CI->load->helper('file'); + + $this->check_path(); + } + + // -------------------------------------------------------------------- + + /** + * Set Cache Directory Path + * + * @param string $path Path to the cache directory + * @return bool + */ + public function check_path($path = '') + { + if ($path === '') + { + if ($this->db->cachedir === '') + { + return $this->db->cache_off(); + } + + $path = $this->db->cachedir; + } + + // Add a trailing slash to the path if needed + $path = realpath($path) + ? rtrim(realpath($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR + : rtrim($path, '/').'/'; + + if ( ! is_dir($path)) + { + log_message('debug', 'DB cache path error: '.$path); + + // If the path is wrong we'll turn off caching + return $this->db->cache_off(); + } + + if ( ! is_really_writable($path)) + { + log_message('debug', 'DB cache dir not writable: '.$path); + + // If the path is not really writable we'll turn off caching + return $this->db->cache_off(); + } + + $this->db->cachedir = $path; + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Retrieve a cached query + * + * The URI being requested will become the name of the cache sub-folder. + * An MD5 hash of the SQL statement will become the cache file name. + * + * @param string $sql + * @return string + */ + public function read($sql) + { + $segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1); + $segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2); + $filepath = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'.md5($sql); + + if ( ! is_file($filepath) OR FALSE === ($cachedata = file_get_contents($filepath))) + { + return FALSE; + } + + return unserialize($cachedata); + } + + // -------------------------------------------------------------------- + + /** + * Write a query to a cache file + * + * @param string $sql + * @param object $object + * @return bool + */ + public function write($sql, $object) + { + $segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1); + $segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2); + $dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'; + $filename = md5($sql); + + if ( ! is_dir($dir_path) && ! @mkdir($dir_path, 0750)) + { + return FALSE; + } + + if (write_file($dir_path.$filename, serialize($object)) === FALSE) + { + return FALSE; + } + + chmod($dir_path.$filename, 0640); + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Delete cache files within a particular directory + * + * @param string $segment_one + * @param string $segment_two + * @return void + */ + public function delete($segment_one = '', $segment_two = '') + { + if ($segment_one === '') + { + $segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1); + } + + if ($segment_two === '') + { + $segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2); + } + + $dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'; + delete_files($dir_path, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Delete all existing cache files + * + * @return void + */ + public function delete_all() + { + delete_files($this->db->cachedir, TRUE, TRUE); + } + +} diff --git a/system/database/DB_driver.php b/system/database/DB_driver.php new file mode 100644 index 0000000..522f1bb --- /dev/null +++ b/system/database/DB_driver.php @@ -0,0 +1,1999 @@ + $val) + { + $this->$key = $val; + } + } + + log_message('info', 'Database Driver Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Initialize Database Settings + * + * @return bool + */ + public function initialize() + { + /* If an established connection is available, then there's + * no need to connect and select the database. + * + * Depending on the database driver, conn_id can be either + * boolean TRUE, a resource or an object. + */ + if ($this->conn_id) + { + return TRUE; + } + + // ---------------------------------------------------------------- + + // Connect to the database and set the connection ID + $this->conn_id = $this->db_connect($this->pconnect); + + // No connection resource? Check if there is a failover else throw an error + if ( ! $this->conn_id) + { + // Check if there is a failover set + if ( ! empty($this->failover) && is_array($this->failover)) + { + // Go over all the failovers + foreach ($this->failover as $failover) + { + // Replace the current settings with those of the failover + foreach ($failover as $key => $val) + { + $this->$key = $val; + } + + // Try to connect + $this->conn_id = $this->db_connect($this->pconnect); + + // If a connection is made break the foreach loop + if ($this->conn_id) + { + break; + } + } + } + + // We still don't have a connection? + if ( ! $this->conn_id) + { + log_message('error', 'Unable to connect to the database'); + + if ($this->db_debug) + { + $this->display_error('db_unable_to_connect'); + } + + return FALSE; + } + } + + // Now we set the character set and that's all + return $this->db_set_charset($this->char_set); + } + + // -------------------------------------------------------------------- + + /** + * DB connect + * + * This is just a dummy method that all drivers will override. + * + * @return mixed + */ + public function db_connect() + { + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Persistent database connection + * + * @return mixed + */ + public function db_pconnect() + { + return $this->db_connect(TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout. + * + * This is just a dummy method to allow drivers without such + * functionality to not declare it, while others will override it. + * + * @return void + */ + public function reconnect() + { + } + + // -------------------------------------------------------------------- + + /** + * Select database + * + * This is just a dummy method to allow drivers without such + * functionality to not declare it, while others will override it. + * + * @return bool + */ + public function db_select() + { + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Last error + * + * @return array + */ + public function error() + { + return array('code' => NULL, 'message' => NULL); + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string + * @return bool + */ + public function db_set_charset($charset) + { + if (method_exists($this, '_db_set_charset') && ! $this->_db_set_charset($charset)) + { + log_message('error', 'Unable to set database connection charset: '.$charset); + + if ($this->db_debug) + { + $this->display_error('db_unable_to_set_charset', $charset); + } + + return FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * The name of the platform in use (mysql, mssql, etc...) + * + * @return string + */ + public function platform() + { + return $this->dbdriver; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * Returns a string containing the version of the database being used. + * Most drivers will override this method. + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if (FALSE === ($sql = $this->_version())) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + $query = $this->query($sql)->row(); + return $this->data_cache['version'] = $query->ver; + } + + // -------------------------------------------------------------------- + + /** + * Version number query string + * + * @return string + */ + protected function _version() + { + return 'SELECT VERSION() AS ver'; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * Accepts an SQL string as input and returns a result object upon + * successful execution of a "read" type query. Returns boolean TRUE + * upon successful execution of a "write" type query. Returns boolean + * FALSE upon failure, and if the $db_debug variable is set to TRUE + * will raise an error. + * + * @param string $sql + * @param array $binds = FALSE An array of binding data + * @param bool $return_object = NULL + * @return mixed + */ + public function query($sql, $binds = FALSE, $return_object = NULL) + { + if ($sql === '') + { + log_message('error', 'Invalid query: '.$sql); + return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE; + } + elseif ( ! is_bool($return_object)) + { + $return_object = ! $this->is_write_type($sql); + } + + // Verify table prefix and replace if necessary + if ($this->dbprefix !== '' && $this->swap_pre !== '' && $this->dbprefix !== $this->swap_pre) + { + $sql = preg_replace('/(\W)'.$this->swap_pre.'(\S+?)/', '\\1'.$this->dbprefix.'\\2', $sql); + } + + // Compile binds if needed + if ($binds !== FALSE) + { + $sql = $this->compile_binds($sql, $binds); + } + + // Is query caching enabled? If the query is a "read type" + // we will load the caching class and return the previously + // cached query if it exists + if ($this->cache_on === TRUE && $return_object === TRUE && $this->_cache_init()) + { + $this->load_rdriver(); + if (FALSE !== ($cache = $this->CACHE->read($sql))) + { + return $cache; + } + } + + // Save the query for debugging + if ($this->save_queries === TRUE) + { + $this->queries[] = $sql; + } + + // Start the Query Timer + $time_start = microtime(TRUE); + + // Run the Query + if (FALSE === ($this->result_id = $this->simple_query($sql))) + { + if ($this->save_queries === TRUE) + { + $this->query_times[] = 0; + } + + // This will trigger a rollback if transactions are being used + if ($this->_trans_depth !== 0) + { + $this->_trans_status = FALSE; + } + + // Grab the error now, as we might run some additional queries before displaying the error + $error = $this->error(); + + // Log errors + log_message('error', 'Query error: '.$error['message'].' - Invalid query: '.$sql); + + if ($this->db_debug) + { + // We call this function in order to roll-back queries + // if transactions are enabled. If we don't call this here + // the error message will trigger an exit, causing the + // transactions to remain in limbo. + while ($this->_trans_depth !== 0) + { + $trans_depth = $this->_trans_depth; + $this->trans_complete(); + if ($trans_depth === $this->_trans_depth) + { + log_message('error', 'Database: Failure during an automated transaction commit/rollback!'); + break; + } + } + + // Display errors + return $this->display_error(array('Error Number: '.$error['code'], $error['message'], $sql)); + } + + return FALSE; + } + + // Stop and aggregate the query time results + $time_end = microtime(TRUE); + $this->benchmark += $time_end - $time_start; + + if ($this->save_queries === TRUE) + { + $this->query_times[] = $time_end - $time_start; + } + + // Increment the query counter + $this->query_count++; + + // Will we have a result object instantiated? If not - we'll simply return TRUE + if ($return_object !== TRUE) + { + // If caching is enabled we'll auto-cleanup any existing files related to this particular URI + if ($this->cache_on === TRUE && $this->cache_autodel === TRUE && $this->_cache_init()) + { + $this->CACHE->delete(); + } + + return TRUE; + } + + // Load and instantiate the result driver + $driver = $this->load_rdriver(); + $RES = new $driver($this); + + // Is query caching enabled? If so, we'll serialize the + // result object and save it to a cache file. + if ($this->cache_on === TRUE && $this->_cache_init()) + { + // We'll create a new instance of the result object + // only without the platform specific driver since + // we can't use it with cached data (the query result + // resource ID won't be any good once we've cached the + // result object, so we'll have to compile the data + // and save it) + $CR = new CI_DB_result($this); + $CR->result_object = $RES->result_object(); + $CR->result_array = $RES->result_array(); + $CR->num_rows = $RES->num_rows(); + + // Reset these since cached objects can not utilize resource IDs. + $CR->conn_id = NULL; + $CR->result_id = NULL; + + $this->CACHE->write($sql, $CR); + } + + return $RES; + } + + // -------------------------------------------------------------------- + + /** + * Load the result drivers + * + * @return string the name of the result class + */ + public function load_rdriver() + { + $driver = 'CI_DB_'.$this->dbdriver.'_result'; + + if ( ! class_exists($driver, FALSE)) + { + require_once(BASEPATH.'database/DB_result.php'); + require_once(BASEPATH.'database/drivers/'.$this->dbdriver.'/'.$this->dbdriver.'_result.php'); + } + + return $driver; + } + + // -------------------------------------------------------------------- + + /** + * Simple Query + * This is a simplified version of the query() function. Internally + * we only use it when running transaction commands since they do + * not require all the features of the main query() function. + * + * @param string the sql query + * @return mixed + */ + public function simple_query($sql) + { + if ( ! $this->conn_id) + { + if ( ! $this->initialize()) + { + return FALSE; + } + } + + return $this->_execute($sql); + } + + // -------------------------------------------------------------------- + + /** + * Disable Transactions + * This permits transactions to be disabled at run-time. + * + * @return void + */ + public function trans_off() + { + $this->trans_enabled = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Enable/disable Transaction Strict Mode + * + * When strict mode is enabled, if you are running multiple groups of + * transactions, if one group fails all subsequent groups will be + * rolled back. + * + * If strict mode is disabled, each group is treated autonomously, + * meaning a failure of one group will not affect any others + * + * @param bool $mode = TRUE + * @return void + */ + public function trans_strict($mode = TRUE) + { + $this->trans_strict = is_bool($mode) ? $mode : TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Start Transaction + * + * @param bool $test_mode = FALSE + * @return bool + */ + public function trans_start($test_mode = FALSE) + { + if ( ! $this->trans_enabled) + { + return FALSE; + } + + return $this->trans_begin($test_mode); + } + + // -------------------------------------------------------------------- + + /** + * Complete Transaction + * + * @return bool + */ + public function trans_complete() + { + if ( ! $this->trans_enabled) + { + return FALSE; + } + + // The query() function will set this flag to FALSE in the event that a query failed + if ($this->_trans_status === FALSE OR $this->_trans_failure === TRUE) + { + $this->trans_rollback(); + + // If we are NOT running in strict mode, we will reset + // the _trans_status flag so that subsequent groups of + // transactions will be permitted. + if ($this->trans_strict === FALSE) + { + $this->_trans_status = TRUE; + } + + log_message('debug', 'DB Transaction Failure'); + return FALSE; + } + + return $this->trans_commit(); + } + + // -------------------------------------------------------------------- + + /** + * Lets you retrieve the transaction flag to determine if it has failed + * + * @return bool + */ + public function trans_status() + { + return $this->_trans_status; + } + + // -------------------------------------------------------------------- + + /** + * Returns TRUE if a transaction is currently active + * + * @return bool + */ + public function trans_active() + { + return (bool) $this->_trans_depth; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @param bool $test_mode + * @return bool + */ + public function trans_begin($test_mode = FALSE) + { + if ( ! $this->trans_enabled) + { + return FALSE; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->_trans_depth > 0) + { + $this->_trans_depth++; + return TRUE; + } + + // Reset the transaction failure flag. + // If the $test_mode flag is set to TRUE transactions will be rolled back + // even if the queries produce a successful result. + $this->_trans_failure = ($test_mode === TRUE); + + if ($this->_trans_begin()) + { + $this->_trans_status = TRUE; + $this->_trans_depth++; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + public function trans_commit() + { + if ( ! $this->trans_enabled OR $this->_trans_depth === 0) + { + return FALSE; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->_trans_depth > 1 OR $this->_trans_commit()) + { + $this->_trans_depth--; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + public function trans_rollback() + { + if ( ! $this->trans_enabled OR $this->_trans_depth === 0) + { + return FALSE; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->_trans_depth > 1 OR $this->_trans_rollback()) + { + $this->_trans_depth--; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Compile Bindings + * + * @param string the sql statement + * @param array an array of bind data + * @return string + */ + public function compile_binds($sql, $binds) + { + if (empty($this->bind_marker) OR strpos($sql, $this->bind_marker) === FALSE) + { + return $sql; + } + elseif ( ! is_array($binds)) + { + $binds = array($binds); + $bind_count = 1; + } + else + { + // Make sure we're using numeric keys + $binds = array_values($binds); + $bind_count = count($binds); + } + + // We'll need the marker length later + $ml = strlen($this->bind_marker); + + // Make sure not to replace a chunk inside a string that happens to match the bind marker + if ($c = preg_match_all("/'[^']*'|\"[^\"]*\"/i", $sql, $matches)) + { + $c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', + str_replace($matches[0], + str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]), + $sql, $c), + $matches, PREG_OFFSET_CAPTURE); + + // Bind values' count must match the count of markers in the query + if ($bind_count !== $c) + { + return $sql; + } + } + elseif (($c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count) + { + return $sql; + } + + do + { + $c--; + $escaped_value = $this->escape($binds[$c]); + if (is_array($escaped_value)) + { + $escaped_value = '('.implode(',', $escaped_value).')'; + } + $sql = substr_replace($sql, $escaped_value, $matches[0][$c][1], $ml); + } + while ($c !== 0); + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + return (bool) preg_match('/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s/i', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Calculate the aggregate query elapsed time + * + * @param int The number of decimal places + * @return string + */ + public function elapsed_time($decimals = 6) + { + return number_format($this->benchmark, $decimals); + } + + // -------------------------------------------------------------------- + + /** + * Returns the total number of queries + * + * @return int + */ + public function total_queries() + { + return $this->query_count; + } + + // -------------------------------------------------------------------- + + /** + * Returns the last query that was executed + * + * @return string + */ + public function last_query() + { + return end($this->queries); + } + + // -------------------------------------------------------------------- + + /** + * "Smart" Escape String + * + * Escapes data based on type + * Sets boolean and null types + * + * @param string + * @return mixed + */ + public function escape($str) + { + if (is_array($str)) + { + $str = array_map(array(&$this, 'escape'), $str); + return $str; + } + elseif (is_string($str) OR (is_object($str) && method_exists($str, '__toString'))) + { + return "'".$this->escape_str($str)."'"; + } + elseif (is_bool($str)) + { + return ($str === FALSE) ? 0 : 1; + } + elseif ($str === NULL) + { + return 'NULL'; + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Escape String + * + * @param string|string[] $str Input string + * @param bool $like Whether or not the string will be used in a LIKE condition + * @return string + */ + public function escape_str($str, $like = FALSE) + { + if (is_array($str)) + { + foreach ($str as $key => $val) + { + $str[$key] = $this->escape_str($val, $like); + } + + return $str; + } + + $str = $this->_escape_str($str); + + // escape LIKE condition wildcards + if ($like === TRUE) + { + return str_replace( + array($this->_like_escape_chr, '%', '_'), + array($this->_like_escape_chr.$this->_like_escape_chr, $this->_like_escape_chr.'%', $this->_like_escape_chr.'_'), + $str + ); + } + + return $str; + } + + // -------------------------------------------------------------------- + + /** + * Escape LIKE String + * + * Calls the individual driver for platform + * specific escaping for LIKE conditions + * + * @param string|string[] + * @return mixed + */ + public function escape_like_str($str) + { + return $this->escape_str($str, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return str_replace("'", "''", remove_invisible_characters($str, FALSE)); + } + + // -------------------------------------------------------------------- + + /** + * Primary + * + * Retrieves the primary key. It assumes that the row in the first + * position is the primary key + * + * @param string $table Table name + * @return string + */ + public function primary($table) + { + $fields = $this->list_fields($table); + return is_array($fields) ? current($fields) : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * "Count All" query + * + * Generates a platform-specific query string that counts all records in + * the specified database + * + * @param string + * @return int + */ + public function count_all($table = '') + { + if ($table === '') + { + return 0; + } + + $query = $this->query($this->_count_string.$this->escape_identifiers('numrows').' FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE)); + if ($query->num_rows() === 0) + { + return 0; + } + + $query = $query->row(); + $this->_reset_select(); + return (int) $query->numrows; + } + + // -------------------------------------------------------------------- + + /** + * Returns an array of table names + * + * @param string $constrain_by_prefix = FALSE + * @return array + */ + public function list_tables($constrain_by_prefix = FALSE) + { + // Is there a cached result? + if (isset($this->data_cache['table_names'])) + { + return $this->data_cache['table_names']; + } + + if (FALSE === ($sql = $this->_list_tables($constrain_by_prefix))) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + $this->data_cache['table_names'] = array(); + $query = $this->query($sql); + + foreach ($query->result_array() as $row) + { + // Do we know from which column to get the table name? + if ( ! isset($key)) + { + if (isset($row['table_name'])) + { + $key = 'table_name'; + } + elseif (isset($row['TABLE_NAME'])) + { + $key = 'TABLE_NAME'; + } + else + { + /* We have no other choice but to just get the first element's key. + * Due to array_shift() accepting its argument by reference, if + * E_STRICT is on, this would trigger a warning. So we'll have to + * assign it first. + */ + $key = array_keys($row); + $key = array_shift($key); + } + } + + $this->data_cache['table_names'][] = $row[$key]; + } + + return $this->data_cache['table_names']; + } + + // -------------------------------------------------------------------- + + /** + * Determine if a particular table exists + * + * @param string $table_name + * @return bool + */ + public function table_exists($table_name) + { + return in_array($this->protect_identifiers($table_name, TRUE, FALSE, FALSE), $this->list_tables()); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * @param string $table Table name + * @return array + */ + public function list_fields($table) + { + if (FALSE === ($sql = $this->_list_columns($table))) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + $query = $this->query($sql); + $fields = array(); + + foreach ($query->result_array() as $row) + { + // Do we know from where to get the column's name? + if ( ! isset($key)) + { + if (isset($row['column_name'])) + { + $key = 'column_name'; + } + elseif (isset($row['COLUMN_NAME'])) + { + $key = 'COLUMN_NAME'; + } + else + { + // We have no other choice but to just get the first element's key. + $key = key($row); + } + } + + $fields[] = $row[$key]; + } + + return $fields; + } + + // -------------------------------------------------------------------- + + /** + * Determine if a particular field exists + * + * @param string + * @param string + * @return bool + */ + public function field_exists($field_name, $table_name) + { + return in_array($field_name, $this->list_fields($table_name)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table the table name + * @return array + */ + public function field_data($table) + { + $query = $this->query($this->_field_data($this->protect_identifiers($table, TRUE, NULL, FALSE))); + return ($query) ? $query->field_data() : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Escape the SQL Identifiers + * + * This function escapes column and table names + * + * @param mixed + * @return mixed + */ + public function escape_identifiers($item) + { + if ($this->_escape_char === '' OR empty($item) OR in_array($item, $this->_reserved_identifiers)) + { + return $item; + } + elseif (is_array($item)) + { + foreach ($item as $key => $value) + { + $item[$key] = $this->escape_identifiers($value); + } + + return $item; + } + // Avoid breaking functions and literal values inside queries + elseif (ctype_digit($item) OR $item[0] === "'" OR ($this->_escape_char !== '"' && $item[0] === '"') OR strpos($item, '(') !== FALSE) + { + return $item; + } + + static $preg_ec = array(); + + if (empty($preg_ec)) + { + if (is_array($this->_escape_char)) + { + $preg_ec = array( + preg_quote($this->_escape_char[0], '/'), + preg_quote($this->_escape_char[1], '/'), + $this->_escape_char[0], + $this->_escape_char[1] + ); + } + else + { + $preg_ec[0] = $preg_ec[1] = preg_quote($this->_escape_char, '/'); + $preg_ec[2] = $preg_ec[3] = $this->_escape_char; + } + } + + foreach ($this->_reserved_identifiers as $id) + { + if (strpos($item, '.'.$id) !== FALSE) + { + return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?\./i', $preg_ec[2].'$1'.$preg_ec[3].'.', $item); + } + } + + return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?(\.)?/i', $preg_ec[2].'$1'.$preg_ec[3].'$2', $item); + } + + // -------------------------------------------------------------------- + + /** + * Generate an insert string + * + * @param string the table upon which the query will be performed + * @param array an associative array data of key/values + * @return string + */ + public function insert_string($table, $data) + { + $fields = $values = array(); + + foreach ($data as $key => $val) + { + $fields[] = $this->escape_identifiers($key); + $values[] = $this->escape($val); + } + + return $this->_insert($this->protect_identifiers($table, TRUE, NULL, FALSE), $fields, $values); + } + + // -------------------------------------------------------------------- + + /** + * Insert statement + * + * Generates a platform-specific insert string from the supplied data + * + * @param string the table name + * @param array the insert keys + * @param array the insert values + * @return string + */ + protected function _insert($table, $keys, $values) + { + return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + // -------------------------------------------------------------------- + + /** + * Generate an update string + * + * @param string the table upon which the query will be performed + * @param array an associative array data of key/values + * @param mixed the "where" statement + * @return string + */ + public function update_string($table, $data, $where) + { + if (empty($where)) + { + return FALSE; + } + + $this->where($where); + + $fields = array(); + foreach ($data as $key => $val) + { + $fields[$this->protect_identifiers($key)] = $this->escape($val); + } + + $sql = $this->_update($this->protect_identifiers($table, TRUE, NULL, FALSE), $fields); + $this->_reset_write(); + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string the table name + * @param array the update data + * @return string + */ + protected function _update($table, $values) + { + foreach ($values as $key => $val) + { + $valstr[] = $key.' = '.$val; + } + + return 'UPDATE '.$table.' SET '.implode(', ', $valstr) + .$this->_compile_wh('qb_where') + .$this->_compile_order_by() + .($this->qb_limit !== FALSE ? ' LIMIT '.$this->qb_limit : ''); + } + + // -------------------------------------------------------------------- + + /** + * Tests whether the string has an SQL operator + * + * @param string + * @return bool + */ + protected function _has_operator($str) + { + return (bool) preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', trim($str)); + } + + // -------------------------------------------------------------------- + + /** + * Returns the SQL string operator + * + * @param string + * @return string + */ + protected function _get_operator($str) + { + static $_operators; + + if (empty($_operators)) + { + $_les = ($this->_like_escape_str !== '') + ? '\s+'.preg_quote(trim(sprintf($this->_like_escape_str, $this->_like_escape_chr)), '/') + : ''; + $_operators = array( + '\s*(?:<|>|!)?=\s*', // =, <=, >=, != + '\s*<>?\s*', // <, <> + '\s*>\s*', // > + '\s+IS NULL', // IS NULL + '\s+IS NOT NULL', // IS NOT NULL + '\s+EXISTS\s*\(.*\)', // EXISTS(sql) + '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql) + '\s+BETWEEN\s+', // BETWEEN value AND value + '\s+NOT BETWEEN\s+', // NOT BETWEEN value AND value + '\s+IN\s*\(.*\)', // IN(list) + '\s+NOT IN\s*\(.*\)', // NOT IN (list) + '\s+LIKE\s+\S.*('.$_les.')?', // LIKE 'expr'[ ESCAPE '%s'] + '\s+NOT LIKE\s+\S.*('.$_les.')?' // NOT LIKE 'expr'[ ESCAPE '%s'] + ); + + } + + return preg_match('/'.implode('|', $_operators).'/i', $str, $match) + ? $match[0] : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Enables a native PHP function to be run, using a platform agnostic wrapper. + * + * @param string $function Function name + * @return mixed + */ + public function call_function($function) + { + $driver = ($this->dbdriver === 'postgre') ? 'pg_' : $this->dbdriver.'_'; + + if (FALSE === strpos($driver, $function)) + { + $function = $driver.$function; + } + + if ( ! function_exists($function)) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE; + } + + return (func_num_args() > 1) + ? call_user_func_array($function, array_slice(func_get_args(), 1)) + : call_user_func($function); + } + + // -------------------------------------------------------------------- + + /** + * Set Cache Directory Path + * + * @param string the path to the cache directory + * @return void + */ + public function cache_set_path($path = '') + { + $this->cachedir = $path; + } + + // -------------------------------------------------------------------- + + /** + * Enable Query Caching + * + * @return bool cache_on value + */ + public function cache_on() + { + return $this->cache_on = TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Disable Query Caching + * + * @return bool cache_on value + */ + public function cache_off() + { + return $this->cache_on = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Delete the cache files associated with a particular URI + * + * @param string $segment_one = '' + * @param string $segment_two = '' + * @return bool + */ + public function cache_delete($segment_one = '', $segment_two = '') + { + return $this->_cache_init() + ? $this->CACHE->delete($segment_one, $segment_two) + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Delete All cache files + * + * @return bool + */ + public function cache_delete_all() + { + return $this->_cache_init() + ? $this->CACHE->delete_all() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Initialize the Cache Class + * + * @return bool + */ + protected function _cache_init() + { + if ( ! class_exists('CI_DB_Cache', FALSE)) + { + require_once(BASEPATH.'database/DB_cache.php'); + } + elseif (is_object($this->CACHE)) + { + return TRUE; + } + + $this->CACHE = new CI_DB_Cache($this); // pass db object to support multiple db connections and returned db objects + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + public function close() + { + if ($this->conn_id) + { + $this->_close(); + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * This method would be overridden by most of the drivers. + * + * @return void + */ + protected function _close() + { + $this->conn_id = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Display an error message + * + * @param string the error message + * @param string any "swap" values + * @param bool whether to localize the message + * @return string sends the application/views/errors/error_db.php template + */ + public function display_error($error = '', $swap = '', $native = FALSE) + { + $LANG =& load_class('Lang', 'core'); + $LANG->load('db'); + + $heading = $LANG->line('db_error_heading'); + + if ($native === TRUE) + { + $message = (array) $error; + } + else + { + $message = is_array($error) ? $error : array(str_replace('%s', $swap, $LANG->line($error))); + } + + // Find the most likely culprit of the error by going through + // the backtrace until the source file is no longer in the + // database folder. + $trace = debug_backtrace(); + foreach ($trace as $call) + { + if (isset($call['file'], $call['class'])) + { + // We'll need this on Windows, as APPPATH and BASEPATH will always use forward slashes + if (DIRECTORY_SEPARATOR !== '/') + { + $call['file'] = str_replace('\\', '/', $call['file']); + } + + if (strpos($call['file'], BASEPATH.'database') === FALSE && strpos($call['class'], 'Loader') === FALSE) + { + // Found it - use a relative path for safety + $message[] = 'Filename: '.str_replace(array(APPPATH, BASEPATH), '', $call['file']); + $message[] = 'Line Number: '.$call['line']; + break; + } + } + } + + $error =& load_class('Exceptions', 'core'); + echo $error->show_error($heading, $message, 'error_db'); + exit(8); // EXIT_DATABASE + } + + // -------------------------------------------------------------------- + + /** + * Protect Identifiers + * + * This function is used extensively by the Query Builder class, and by + * a couple functions in this class. + * It takes a column or table name (optionally with an alias) and inserts + * the table prefix onto it. Some logic is necessary in order to deal with + * column names that include the path. Consider a query like this: + * + * SELECT hostname.database.table.column AS c FROM hostname.database.table + * + * Or a query with aliasing: + * + * SELECT m.member_id, m.member_name FROM members AS m + * + * Since the column name can include up to four segments (host, DB, table, column) + * or also have an alias prefix, we need to do a bit of work to figure this out and + * insert the table prefix (if it exists) in the proper position, and escape only + * the correct identifiers. + * + * @param string + * @param bool + * @param mixed + * @param bool + * @return string + */ + public function protect_identifiers($item, $prefix_single = FALSE, $protect_identifiers = NULL, $field_exists = TRUE) + { + if ( ! is_bool($protect_identifiers)) + { + $protect_identifiers = $this->_protect_identifiers; + } + + if (is_array($item)) + { + $escaped_array = array(); + foreach ($item as $k => $v) + { + $escaped_array[$this->protect_identifiers($k)] = $this->protect_identifiers($v, $prefix_single, $protect_identifiers, $field_exists); + } + + return $escaped_array; + } + + // This is basically a bug fix for queries that use MAX, MIN, etc. + // If a parenthesis is found we know that we do not need to + // escape the data or add a prefix. There's probably a more graceful + // way to deal with this, but I'm not thinking of it -- Rick + // + // Added exception for single quotes as well, we don't want to alter + // literal strings. -- Narf + if (strcspn($item, "()'") !== strlen($item)) + { + return $item; + } + + // Convert tabs or multiple spaces into single spaces + $item = preg_replace('/\s+/', ' ', trim($item)); + + // If the item has an alias declaration we remove it and set it aside. + // Note: strripos() is used in order to support spaces in table names + if ($offset = strripos($item, ' AS ')) + { + $alias = ($protect_identifiers) + ? substr($item, $offset, 4).$this->escape_identifiers(substr($item, $offset + 4)) + : substr($item, $offset); + $item = substr($item, 0, $offset); + } + elseif ($offset = strrpos($item, ' ')) + { + $alias = ($protect_identifiers) + ? ' '.$this->escape_identifiers(substr($item, $offset + 1)) + : substr($item, $offset); + $item = substr($item, 0, $offset); + } + else + { + $alias = ''; + } + + // Break the string apart if it contains periods, then insert the table prefix + // in the correct location, assuming the period doesn't indicate that we're dealing + // with an alias. While we're at it, we will escape the components + if (strpos($item, '.') !== FALSE) + { + $parts = explode('.', $item); + + // Does the first segment of the exploded item match + // one of the aliases previously identified? If so, + // we have nothing more to do other than escape the item + // + // NOTE: The ! empty() condition prevents this method + // from breaking when QB isn't enabled. + if ( ! empty($this->qb_aliased_tables) && in_array($parts[0], $this->qb_aliased_tables)) + { + if ($protect_identifiers === TRUE) + { + foreach ($parts as $key => $val) + { + if ( ! in_array($val, $this->_reserved_identifiers)) + { + $parts[$key] = $this->escape_identifiers($val); + } + } + + $item = implode('.', $parts); + } + + return $item.$alias; + } + + // Is there a table prefix defined in the config file? If not, no need to do anything + if ($this->dbprefix !== '') + { + // We now add the table prefix based on some logic. + // Do we have 4 segments (hostname.database.table.column)? + // If so, we add the table prefix to the column name in the 3rd segment. + if (isset($parts[3])) + { + $i = 2; + } + // Do we have 3 segments (database.table.column)? + // If so, we add the table prefix to the column name in 2nd position + elseif (isset($parts[2])) + { + $i = 1; + } + // Do we have 2 segments (table.column)? + // If so, we add the table prefix to the column name in 1st segment + else + { + $i = 0; + } + + // This flag is set when the supplied $item does not contain a field name. + // This can happen when this function is being called from a JOIN. + if ($field_exists === FALSE) + { + $i++; + } + + // dbprefix may've already been applied, with or without the identifier escaped + $ec = '(?'.preg_quote(is_array($this->_escape_char) ? $this->_escape_char[0] : $this->_escape_char).')?'; + isset($ec[0]) && $ec .= '?'; // Just in case someone has disabled escaping by forcing an empty escape character + + // Verify table prefix and replace if necessary + if ($this->swap_pre !== '' && preg_match('#^'.$ec.preg_quote($this->swap_pre).'#', $parts[$i])) + { + $parts[$i] = preg_replace('#^'.$ec.preg_quote($this->swap_pre).'(\S+?)#', '\\1'.$this->dbprefix.'\\2', $parts[$i]); + } + // We only add the table prefix if it does not already exist + else + { + preg_match('#^'.$ec.preg_quote($this->dbprefix).'#', $parts[$i]) OR $parts[$i] = $this->dbprefix.$parts[$i]; + } + + // Put the parts back together + $item = implode('.', $parts); + } + + if ($protect_identifiers === TRUE) + { + $item = $this->escape_identifiers($item); + } + + return $item.$alias; + } + + // Is there a table prefix? If not, no need to insert it + if ($this->dbprefix !== '') + { + // Verify table prefix and replace if necessary + if ($this->swap_pre !== '' && strpos($item, $this->swap_pre) === 0) + { + $item = preg_replace('/^'.$this->swap_pre.'(\S+?)/', $this->dbprefix.'\\1', $item); + } + // Do we prefix an item with no segments? + elseif ($prefix_single === TRUE && strpos($item, $this->dbprefix) !== 0) + { + $item = $this->dbprefix.$item; + } + } + + if ($protect_identifiers === TRUE && ! in_array($item, $this->_reserved_identifiers)) + { + $item = $this->escape_identifiers($item); + } + + return $item.$alias; + } + + // -------------------------------------------------------------------- + + /** + * Dummy method that allows Query Builder class to be disabled + * and keep count_all() working. + * + * @return void + */ + protected function _reset_select() + { + } + +} diff --git a/system/database/DB_forge.php b/system/database/DB_forge.php new file mode 100644 index 0000000..64ccde0 --- /dev/null +++ b/system/database/DB_forge.php @@ -0,0 +1,1034 @@ +db =& $db; + log_message('info', 'Database Forge Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return bool + */ + public function create_database($db_name) + { + if ($this->_create_database === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + elseif ( ! $this->db->query(sprintf($this->_create_database, $this->db->escape_identifiers($db_name), $this->db->char_set, $this->db->dbcollat))) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + if ( ! empty($this->db->data_cache['db_names'])) + { + $this->db->data_cache['db_names'][] = $db_name; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name + * @return bool + */ + public function drop_database($db_name) + { + if ($this->_drop_database === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + elseif ( ! $this->db->query(sprintf($this->_drop_database, $this->db->escape_identifiers($db_name)))) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + if ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($db_name), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Add Key + * + * @param string $key + * @param bool $primary + * @return CI_DB_forge + */ + public function add_key($key, $primary = FALSE) + { + // DO NOT change this! This condition is only applicable + // for PRIMARY keys because you can only have one such, + // and therefore all fields you add to it will be included + // in the same, composite PRIMARY KEY. + // + // It's not the same for regular indexes. + if ($primary === TRUE && is_array($key)) + { + foreach ($key as $one) + { + $this->add_key($one, $primary); + } + + return $this; + } + + if ($primary === TRUE) + { + $this->primary_keys[] = $key; + } + else + { + $this->keys[] = $key; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Add Field + * + * @param array $field + * @return CI_DB_forge + */ + public function add_field($field) + { + if (is_string($field)) + { + if ($field === 'id') + { + $this->add_field(array( + 'id' => array( + 'type' => 'INT', + 'constraint' => 9, + 'auto_increment' => TRUE + ) + )); + $this->add_key('id', TRUE); + } + else + { + if (strpos($field, ' ') === FALSE) + { + show_error('Field information is required for that operation.'); + } + + $this->fields[] = $field; + } + } + + if (is_array($field)) + { + $this->fields = array_merge($this->fields, $field); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Create Table + * + * @param string $table Table name + * @param bool $if_not_exists Whether to add IF NOT EXISTS condition + * @param array $attributes Associative array of table attributes + * @return bool + */ + public function create_table($table, $if_not_exists = FALSE, array $attributes = array()) + { + if ($table === '') + { + show_error('A table name is required for that operation.'); + } + else + { + $table = $this->db->dbprefix.$table; + } + + if (count($this->fields) === 0) + { + show_error('Field information is required.'); + } + + $sql = $this->_create_table($table, $if_not_exists, $attributes); + + if (is_bool($sql)) + { + $this->_reset(); + if ($sql === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + } + + if (($result = $this->db->query($sql)) !== FALSE) + { + if (isset($this->db->data_cache['table_names'])) + { + $this->db->data_cache['table_names'][] = $table; + } + + // Most databases don't support creating indexes from within the CREATE TABLE statement + if ( ! empty($this->keys)) + { + for ($i = 0, $sqls = $this->_process_indexes($table), $c = count($sqls); $i < $c; $i++) + { + $this->db->query($sqls[$i]); + } + } + } + + $this->_reset(); + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Create Table + * + * @param string $table Table name + * @param bool $if_not_exists Whether to add 'IF NOT EXISTS' condition + * @param array $attributes Associative array of table attributes + * @return mixed + */ + protected function _create_table($table, $if_not_exists, $attributes) + { + if ($if_not_exists === TRUE && $this->_create_table_if === FALSE) + { + if ($this->db->table_exists($table)) + { + return TRUE; + } + + $if_not_exists = FALSE; + } + + $sql = ($if_not_exists) + ? sprintf($this->_create_table_if, $this->db->escape_identifiers($table)) + : 'CREATE TABLE'; + + $columns = $this->_process_fields(TRUE); + for ($i = 0, $c = count($columns); $i < $c; $i++) + { + $columns[$i] = ($columns[$i]['_literal'] !== FALSE) + ? "\n\t".$columns[$i]['_literal'] + : "\n\t".$this->_process_column($columns[$i]); + } + + $columns = implode(',', $columns) + .$this->_process_primary_keys($table); + + // Are indexes created from within the CREATE TABLE statement? (e.g. in MySQL) + if ($this->_create_table_keys === TRUE) + { + $columns .= $this->_process_indexes($table); + } + + // _create_table will usually have the following format: "%s %s (%s\n)" + $sql = sprintf($this->_create_table.'%s', + $sql, + $this->db->escape_identifiers($table), + $columns, + $this->_create_table_attr($attributes) + ); + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * CREATE TABLE attributes + * + * @param array $attributes Associative array of table attributes + * @return string + */ + protected function _create_table_attr($attributes) + { + $sql = ''; + + foreach (array_keys($attributes) as $key) + { + if (is_string($key)) + { + $sql .= ' '.strtoupper($key).' '.$attributes[$key]; + } + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Drop Table + * + * @param string $table_name Table name + * @param bool $if_exists Whether to add an IF EXISTS condition + * @return bool + */ + public function drop_table($table_name, $if_exists = FALSE) + { + if ($table_name === '') + { + return ($this->db->db_debug) ? $this->db->display_error('db_table_name_required') : FALSE; + } + + if (($query = $this->_drop_table($this->db->dbprefix.$table_name, $if_exists)) === TRUE) + { + return TRUE; + } + + $query = $this->db->query($query); + + // Update table list cache + if ($query && ! empty($this->db->data_cache['table_names'])) + { + $key = array_search(strtolower($this->db->dbprefix.$table_name), array_map('strtolower', $this->db->data_cache['table_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['table_names'][$key]); + } + } + + return $query; + } + + // -------------------------------------------------------------------- + + /** + * Drop Table + * + * Generates a platform-specific DROP TABLE string + * + * @param string $table Table name + * @param bool $if_exists Whether to add an IF EXISTS condition + * @return mixed (Returns a platform-specific DROP table string, or TRUE to indicate there's nothing to do) + */ + protected function _drop_table($table, $if_exists) + { + $sql = 'DROP TABLE'; + + if ($if_exists) + { + if ($this->_drop_table_if === FALSE) + { + if ( ! $this->db->table_exists($table)) + { + return TRUE; + } + } + else + { + $sql = sprintf($this->_drop_table_if, $this->db->escape_identifiers($table)); + } + } + + return $sql.' '.$this->db->escape_identifiers($table); + } + + // -------------------------------------------------------------------- + + /** + * Rename Table + * + * @param string $table_name Old table name + * @param string $new_table_name New table name + * @return bool + */ + public function rename_table($table_name, $new_table_name) + { + if ($table_name === '' OR $new_table_name === '') + { + show_error('A table name is required for that operation.'); + return FALSE; + } + elseif ($this->_rename_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $result = $this->db->query(sprintf($this->_rename_table, + $this->db->escape_identifiers($this->db->dbprefix.$table_name), + $this->db->escape_identifiers($this->db->dbprefix.$new_table_name)) + ); + + if ($result && ! empty($this->db->data_cache['table_names'])) + { + $key = array_search(strtolower($this->db->dbprefix.$table_name), array_map('strtolower', $this->db->data_cache['table_names']), TRUE); + if ($key !== FALSE) + { + $this->db->data_cache['table_names'][$key] = $this->db->dbprefix.$new_table_name; + } + } + + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Column Add + * + * @todo Remove deprecated $_after option in 3.1+ + * @param string $table Table name + * @param array $field Column definition + * @param string $_after Column for AFTER clause (deprecated) + * @return bool + */ + public function add_column($table, $field, $_after = NULL) + { + // Work-around for literal column definitions + is_array($field) OR $field = array($field); + + foreach (array_keys($field) as $k) + { + // Backwards-compatibility work-around for MySQL/CUBRID AFTER clause (remove in 3.1+) + if ($_after !== NULL && is_array($field[$k]) && ! isset($field[$k]['after'])) + { + $field[$k]['after'] = $_after; + } + + $this->add_field(array($k => $field[$k])); + } + + $sqls = $this->_alter_table('ADD', $this->db->dbprefix.$table, $this->_process_fields()); + $this->_reset(); + if ($sqls === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + for ($i = 0, $c = count($sqls); $i < $c; $i++) + { + if ($this->db->query($sqls[$i]) === FALSE) + { + return FALSE; + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Column Drop + * + * @param string $table Table name + * @param string $column_name Column name + * @return bool + */ + public function drop_column($table, $column_name) + { + $sql = $this->_alter_table('DROP', $this->db->dbprefix.$table, $column_name); + if ($sql === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + return $this->db->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Column Modify + * + * @param string $table Table name + * @param string $field Column definition + * @return bool + */ + public function modify_column($table, $field) + { + // Work-around for literal column definitions + is_array($field) OR $field = array($field); + + foreach (array_keys($field) as $k) + { + $this->add_field(array($k => $field[$k])); + } + + if (count($this->fields) === 0) + { + show_error('Field information is required.'); + } + + $sqls = $this->_alter_table('CHANGE', $this->db->dbprefix.$table, $this->_process_fields()); + $this->_reset(); + if ($sqls === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + for ($i = 0, $c = count($sqls); $i < $c; $i++) + { + if ($this->db->query($sqls[$i]) === FALSE) + { + return FALSE; + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' '; + + // DROP has everything it needs now. + if ($alter_type === 'DROP') + { + return $sql.'DROP COLUMN '.$this->db->escape_identifiers($field); + } + + $sql .= ($alter_type === 'ADD') + ? 'ADD ' + : $alter_type.' COLUMN '; + + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql + .($field[$i]['_literal'] !== FALSE ? $field[$i]['_literal'] : $this->_process_column($field[$i])); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process fields + * + * @param bool $create_table + * @return array + */ + protected function _process_fields($create_table = FALSE) + { + $fields = array(); + + foreach ($this->fields as $key => $attributes) + { + if (is_int($key) && ! is_array($attributes)) + { + $fields[] = array('_literal' => $attributes); + continue; + } + + $attributes = array_change_key_case($attributes, CASE_UPPER); + + if ($create_table === TRUE && empty($attributes['TYPE'])) + { + continue; + } + + isset($attributes['TYPE']) && $this->_attr_type($attributes); + + $field = array( + 'name' => $key, + 'new_name' => isset($attributes['NAME']) ? $attributes['NAME'] : NULL, + 'type' => isset($attributes['TYPE']) ? $attributes['TYPE'] : NULL, + 'length' => '', + 'unsigned' => '', + 'null' => NULL, + 'unique' => '', + 'default' => '', + 'auto_increment' => '', + '_literal' => FALSE + ); + + isset($attributes['TYPE']) && $this->_attr_unsigned($attributes, $field); + + if ($create_table === FALSE) + { + if (isset($attributes['AFTER'])) + { + $field['after'] = $attributes['AFTER']; + } + elseif (isset($attributes['FIRST'])) + { + $field['first'] = (bool) $attributes['FIRST']; + } + } + + $this->_attr_default($attributes, $field); + + if (isset($attributes['NULL'])) + { + if ($attributes['NULL'] === TRUE) + { + $field['null'] = empty($this->_null) ? '' : ' '.$this->_null; + } + else + { + $field['null'] = ' NOT NULL'; + } + } + elseif ($create_table === TRUE) + { + $field['null'] = ' NOT NULL'; + } + + $this->_attr_auto_increment($attributes, $field); + $this->_attr_unique($attributes, $field); + + if (isset($attributes['COMMENT'])) + { + $field['comment'] = $this->db->escape($attributes['COMMENT']); + } + + if (isset($attributes['TYPE']) && ! empty($attributes['CONSTRAINT'])) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']); + default: + $field['length'] = is_array($attributes['CONSTRAINT']) + ? '('.implode(',', $attributes['CONSTRAINT']).')' + : '('.$attributes['CONSTRAINT'].')'; + break; + } + } + + $fields[] = $field; + } + + return $fields; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['default'] + .$field['null'] + .$field['auto_increment'] + .$field['unique']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + // Usually overridden by drivers + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNSIGNED + * + * Depending on the _unsigned property value: + * + * - TRUE will always set $field['unsigned'] to 'UNSIGNED' + * - FALSE will always set $field['unsigned'] to '' + * - array(TYPE) will set $field['unsigned'] to 'UNSIGNED', + * if $attributes['TYPE'] is found in the array + * - array(TYPE => UTYPE) will change $field['type'], + * from TYPE to UTYPE in case of a match + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unsigned(&$attributes, &$field) + { + if (empty($attributes['UNSIGNED']) OR $attributes['UNSIGNED'] !== TRUE) + { + return; + } + + // Reset the attribute in order to avoid issues if we do type conversion + $attributes['UNSIGNED'] = FALSE; + + if (is_array($this->_unsigned)) + { + foreach (array_keys($this->_unsigned) as $key) + { + if (is_int($key) && strcasecmp($attributes['TYPE'], $this->_unsigned[$key]) === 0) + { + $field['unsigned'] = ' UNSIGNED'; + return; + } + elseif (is_string($key) && strcasecmp($attributes['TYPE'], $key) === 0) + { + $field['type'] = $key; + return; + } + } + + return; + } + + $field['unsigned'] = ($this->_unsigned === TRUE) ? ' UNSIGNED' : ''; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute DEFAULT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_default(&$attributes, &$field) + { + if ($this->_default === FALSE) + { + return; + } + + if (array_key_exists('DEFAULT', $attributes)) + { + if ($attributes['DEFAULT'] === NULL) + { + $field['default'] = empty($this->_null) ? '' : $this->_default.$this->_null; + + // Override the NULL attribute if that's our default + $attributes['NULL'] = TRUE; + $field['null'] = empty($this->_null) ? '' : ' '.$this->_null; + } + else + { + $field['default'] = $this->_default.$this->db->escape($attributes['DEFAULT']); + } + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE'; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' AUTO_INCREMENT'; + } + } + + // -------------------------------------------------------------------- + + /** + * Process primary keys + * + * @param string $table Table name + * @return string + */ + protected function _process_primary_keys($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->primary_keys); $i < $c; $i++) + { + if ( ! isset($this->fields[$this->primary_keys[$i]])) + { + unset($this->primary_keys[$i]); + } + } + + if (count($this->primary_keys) > 0) + { + $sql .= ",\n\tCONSTRAINT ".$this->db->escape_identifiers('pk_'.$table) + .' PRIMARY KEY('.implode(', ', $this->db->escape_identifiers($this->primary_keys)).')'; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table Table name + * @return string[] list of SQL statements + */ + protected function _process_indexes($table) + { + $sqls = array(); + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sqls[] = 'CREATE INDEX '.$this->db->escape_identifiers($table.'_'.implode('_', $this->keys[$i])) + .' ON '.$this->db->escape_identifiers($table) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).');'; + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Reset + * + * Resets table creation vars + * + * @return void + */ + protected function _reset() + { + $this->fields = $this->keys = $this->primary_keys = array(); + } + +} diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php new file mode 100644 index 0000000..9331084 --- /dev/null +++ b/system/database/DB_query_builder.php @@ -0,0 +1,2809 @@ +_protect_identifiers; + + foreach ($select as $val) + { + $val = trim($val); + + if ($val !== '') + { + $this->qb_select[] = $val; + $this->qb_no_escape[] = $escape; + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_select[] = $val; + $this->qb_cache_exists[] = 'select'; + $this->qb_cache_no_escape[] = $escape; + } + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Select Max + * + * Generates a SELECT MAX(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_max($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'MAX'); + } + + // -------------------------------------------------------------------- + + /** + * Select Min + * + * Generates a SELECT MIN(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_min($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'MIN'); + } + + // -------------------------------------------------------------------- + + /** + * Select Average + * + * Generates a SELECT AVG(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_avg($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'AVG'); + } + + // -------------------------------------------------------------------- + + /** + * Select Sum + * + * Generates a SELECT SUM(field) portion of a query + * + * @param string the field + * @param string an alias + * @return CI_DB_query_builder + */ + public function select_sum($select = '', $alias = '') + { + return $this->_max_min_avg_sum($select, $alias, 'SUM'); + } + + // -------------------------------------------------------------------- + + /** + * SELECT [MAX|MIN|AVG|SUM]() + * + * @used-by select_max() + * @used-by select_min() + * @used-by select_avg() + * @used-by select_sum() + * + * @param string $select Field name + * @param string $alias + * @param string $type + * @return CI_DB_query_builder + */ + protected function _max_min_avg_sum($select = '', $alias = '', $type = 'MAX') + { + if ( ! is_string($select) OR $select === '') + { + $this->display_error('db_invalid_query'); + } + + $type = strtoupper($type); + + if ( ! in_array($type, array('MAX', 'MIN', 'AVG', 'SUM'))) + { + show_error('Invalid function type: '.$type); + } + + if ($alias === '') + { + $alias = $this->_create_alias_from_table(trim($select)); + } + + $sql = $type.'('.$this->protect_identifiers(trim($select)).') AS '.$this->escape_identifiers(trim($alias)); + + $this->qb_select[] = $sql; + $this->qb_no_escape[] = NULL; + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_select[] = $sql; + $this->qb_cache_exists[] = 'select'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Determines the alias name based on the table + * + * @param string $item + * @return string + */ + protected function _create_alias_from_table($item) + { + if (strpos($item, '.') !== FALSE) + { + $item = explode('.', $item); + return end($item); + } + + return $item; + } + + // -------------------------------------------------------------------- + + /** + * DISTINCT + * + * Sets a flag which tells the query string compiler to add DISTINCT + * + * @param bool $val + * @return CI_DB_query_builder + */ + public function distinct($val = TRUE) + { + $this->qb_distinct = is_bool($val) ? $val : TRUE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * From + * + * Generates the FROM portion of the query + * + * @param mixed $from can be a string or array + * @return CI_DB_query_builder + */ + public function from($from) + { + foreach ((array) $from as $val) + { + if (strpos($val, ',') !== FALSE) + { + foreach (explode(',', $val) as $v) + { + $v = trim($v); + $this->_track_aliases($v); + + $this->qb_from[] = $v = $this->protect_identifiers($v, TRUE, NULL, FALSE); + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_from[] = $v; + $this->qb_cache_exists[] = 'from'; + } + } + } + else + { + $val = trim($val); + + // Extract any aliases that might exist. We use this information + // in the protect_identifiers to know whether to add a table prefix + $this->_track_aliases($val); + + $this->qb_from[] = $val = $this->protect_identifiers($val, TRUE, NULL, FALSE); + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_from[] = $val; + $this->qb_cache_exists[] = 'from'; + } + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * JOIN + * + * Generates the JOIN portion of the query + * + * @param string + * @param string the join condition + * @param string the type of join + * @param string whether not to try to escape identifiers + * @return CI_DB_query_builder + */ + public function join($table, $cond, $type = '', $escape = NULL) + { + if ($type !== '') + { + $type = strtoupper(trim($type)); + + if ( ! in_array($type, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER', 'FULL OUTER', 'FULL'), TRUE)) + { + $type = ''; + } + else + { + $type .= ' '; + } + } + + // Extract any aliases that might exist. We use this information + // in the protect_identifiers to know whether to add a table prefix + $this->_track_aliases($table); + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + if ( ! $this->_has_operator($cond)) + { + $cond = ' USING ('.($escape ? $this->escape_identifiers($cond) : $cond).')'; + } + elseif ($escape === FALSE) + { + $cond = ' ON '.$cond; + } + else + { + // Split multiple conditions + if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE)) + { + $conditions = array(); + $joints = $joints[0]; + array_unshift($joints, array('', 0)); + + for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--) + { + $joints[$i][1] += strlen($joints[$i][0]); // offset + $conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]); + $pos = $joints[$i][1] - strlen($joints[$i][0]); + $joints[$i] = $joints[$i][0]; + } + } + else + { + $conditions = array($cond); + $joints = array(''); + } + + $cond = ' ON '; + for ($i = 0, $c = count($conditions); $i < $c; $i++) + { + $operator = $this->_get_operator($conditions[$i]); + $cond .= $joints[$i]; + $cond .= preg_match("/(\(*)?([\[\]\w\.'-]+)".preg_quote($operator)."(.*)/i", $conditions[$i], $match) + ? $match[1].$this->protect_identifiers($match[2]).$operator.$this->protect_identifiers($match[3]) + : $conditions[$i]; + } + } + + // Do we want to escape the table name? + if ($escape === TRUE) + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // Assemble the JOIN statement + $this->qb_join[] = $join = $type.'JOIN '.$table.$cond; + + if ($this->qb_caching === TRUE) + { + $this->qb_cache_join[] = $join; + $this->qb_cache_exists[] = 'join'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * WHERE + * + * Generates the WHERE portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed + * @param mixed + * @param bool + * @return CI_DB_query_builder + */ + public function where($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_where', $key, $value, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR WHERE + * + * Generates the WHERE portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed + * @param mixed + * @param bool + * @return CI_DB_query_builder + */ + public function or_where($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_where', $key, $value, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * WHERE, HAVING + * + * @used-by where() + * @used-by or_where() + * @used-by having() + * @used-by or_having() + * + * @param string $qb_key 'qb_where' or 'qb_having' + * @param mixed $key + * @param mixed $value + * @param string $type + * @param bool $escape + * @return CI_DB_query_builder + */ + protected function _wh($qb_key, $key, $value = NULL, $type = 'AND ', $escape = NULL) + { + $qb_cache_key = ($qb_key === 'qb_having') ? 'qb_cache_having' : 'qb_cache_where'; + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + // If the escape value was not set will base it on the global setting + is_bool($escape) OR $escape = $this->_protect_identifiers; + + foreach ($key as $k => $v) + { + $prefix = (count($this->$qb_key) === 0 && count($this->$qb_cache_key) === 0) + ? $this->_group_get_type('') + : $this->_group_get_type($type); + + if ($v !== NULL) + { + if ($escape === TRUE) + { + $v = $this->escape($v); + } + + if ( ! $this->_has_operator($k)) + { + $k .= ' = '; + } + } + elseif ( ! $this->_has_operator($k)) + { + // value appears not to have been set, assign the test to IS NULL + $k .= ' IS NULL'; + } + elseif (preg_match('/\s*(!?=|<>|\sIS(?:\s+NOT)?\s)\s*$/i', $k, $match, PREG_OFFSET_CAPTURE)) + { + $k = substr($k, 0, $match[0][1]).($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL'); + } + + ${$qb_key} = array('condition' => $prefix.$k, 'value' => $v, 'escape' => $escape); + $this->{$qb_key}[] = ${$qb_key}; + if ($this->qb_caching === TRUE) + { + $this->{$qb_cache_key}[] = ${$qb_key}; + $this->qb_cache_exists[] = substr($qb_key, 3); + } + + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * WHERE IN + * + * Generates a WHERE field IN('item', 'item') SQL query, + * joined with 'AND' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function where_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, FALSE, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR WHERE IN + * + * Generates a WHERE field IN('item', 'item') SQL query, + * joined with 'OR' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_where_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, FALSE, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * WHERE NOT IN + * + * Generates a WHERE field NOT IN('item', 'item') SQL query, + * joined with 'AND' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function where_not_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, TRUE, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR WHERE NOT IN + * + * Generates a WHERE field NOT IN('item', 'item') SQL query, + * joined with 'OR' if appropriate. + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_where_not_in($key = NULL, $values = NULL, $escape = NULL) + { + return $this->_where_in($key, $values, TRUE, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * Internal WHERE IN + * + * @used-by where_in() + * @used-by or_where_in() + * @used-by where_not_in() + * @used-by or_where_not_in() + * + * @param string $key The field to search + * @param array $values The values searched on + * @param bool $not If the statement would be IN or NOT IN + * @param string $type + * @param bool $escape + * @return CI_DB_query_builder + */ + protected function _where_in($key = NULL, $values = NULL, $not = FALSE, $type = 'AND ', $escape = NULL) + { + if ($key === NULL OR $values === NULL) + { + return $this; + } + + if ( ! is_array($values)) + { + $values = array($values); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + $not = ($not) ? ' NOT' : ''; + + if ($escape === TRUE) + { + $where_in = array(); + foreach ($values as $value) + { + $where_in[] = $this->escape($value); + } + } + else + { + $where_in = array_values($values); + } + + $prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) + ? $this->_group_get_type('') + : $this->_group_get_type($type); + + $where_in = array( + 'condition' => $prefix.$key.$not.' IN('.implode(', ', $where_in).')', + 'value' => NULL, + 'escape' => $escape + ); + + $this->qb_where[] = $where_in; + if ($this->qb_caching === TRUE) + { + $this->qb_cache_where[] = $where_in; + $this->qb_cache_exists[] = 'where'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * LIKE + * + * Generates a %LIKE% portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'AND ', $side, '', $escape); + } + + // -------------------------------------------------------------------- + + /** + * NOT LIKE + * + * Generates a NOT LIKE portion of the query. + * Separates multiple calls with 'AND'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function not_like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR LIKE + * + * Generates a %LIKE% portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'OR ', $side, '', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR NOT LIKE + * + * Generates a NOT LIKE portion of the query. + * Separates multiple calls with 'OR'. + * + * @param mixed $field + * @param string $match + * @param string $side + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_not_like($field, $match = '', $side = 'both', $escape = NULL) + { + return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape); + } + + // -------------------------------------------------------------------- + + /** + * Internal LIKE + * + * @used-by like() + * @used-by or_like() + * @used-by not_like() + * @used-by or_not_like() + * + * @param mixed $field + * @param string $match + * @param string $type + * @param string $side + * @param string $not + * @param bool $escape + * @return CI_DB_query_builder + */ + protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '', $escape = NULL) + { + if ( ! is_array($field)) + { + $field = array($field => $match); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + // lowercase $side in case somebody writes e.g. 'BEFORE' instead of 'before' (doh) + $side = strtolower($side); + + foreach ($field as $k => $v) + { + $prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) + ? $this->_group_get_type('') : $this->_group_get_type($type); + + if ($escape === TRUE) + { + $v = $this->escape_like_str($v); + } + + switch ($side) + { + case 'none': + $v = "'{$v}'"; + break; + case 'before': + $v = "'%{$v}'"; + break; + case 'after': + $v = "'{$v}%'"; + break; + case 'both': + default: + $v = "'%{$v}%'"; + break; + } + + // some platforms require an escape sequence definition for LIKE wildcards + if ($escape === TRUE && $this->_like_escape_str !== '') + { + $v .= sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + $qb_where = array('condition' => "{$prefix} {$k} {$not} LIKE {$v}", 'value' => NULL, 'escape' => $escape); + $this->qb_where[] = $qb_where; + if ($this->qb_caching === TRUE) + { + $this->qb_cache_where[] = $qb_where; + $this->qb_cache_exists[] = 'where'; + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group. + * + * @param string $not (Internal use only) + * @param string $type (Internal use only) + * @return CI_DB_query_builder + */ + public function group_start($not = '', $type = 'AND ') + { + $type = $this->_group_get_type($type); + + $this->qb_where_group_started = TRUE; + $prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) ? '' : $type; + $where = array( + 'condition' => $prefix.$not.str_repeat(' ', ++$this->qb_where_group_count).' (', + 'value' => NULL, + 'escape' => FALSE + ); + + $this->qb_where[] = $where; + if ($this->qb_caching) + { + $this->qb_cache_where[] = $where; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group, but ORs the group + * + * @return CI_DB_query_builder + */ + public function or_group_start() + { + return $this->group_start('', 'OR '); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group, but NOTs the group + * + * @return CI_DB_query_builder + */ + public function not_group_start() + { + return $this->group_start('NOT ', 'AND '); + } + + // -------------------------------------------------------------------- + + /** + * Starts a query group, but OR NOTs the group + * + * @return CI_DB_query_builder + */ + public function or_not_group_start() + { + return $this->group_start('NOT ', 'OR '); + } + + // -------------------------------------------------------------------- + + /** + * Ends a query group + * + * @return CI_DB_query_builder + */ + public function group_end() + { + $this->qb_where_group_started = FALSE; + $where = array( + 'condition' => str_repeat(' ', $this->qb_where_group_count--).')', + 'value' => NULL, + 'escape' => FALSE + ); + + $this->qb_where[] = $where; + if ($this->qb_caching) + { + $this->qb_cache_where[] = $where; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Group_get_type + * + * @used-by group_start() + * @used-by _like() + * @used-by _wh() + * @used-by _where_in() + * + * @param string $type + * @return string + */ + protected function _group_get_type($type) + { + if ($this->qb_where_group_started) + { + $type = ''; + $this->qb_where_group_started = FALSE; + } + + return $type; + } + + // -------------------------------------------------------------------- + + /** + * GROUP BY + * + * @param mixed $by + * @param bool $escape + * @return CI_DB_query_builder + */ + public function group_by($by, $escape = NULL) + { + is_bool($escape) OR $escape = $this->_protect_identifiers; + + if (is_string($by)) + { + $by = ($escape === TRUE) + ? explode(',', $by) + : array($by); + } + + foreach ($by as $val) + { + $val = trim($val); + + if ($val !== '') + { + $val = array('field' => $val, 'escape' => $escape); + + $this->qb_groupby[] = $val; + if ($this->qb_caching === TRUE) + { + $this->qb_cache_groupby[] = $val; + $this->qb_cache_exists[] = 'groupby'; + } + } + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * HAVING + * + * Separates multiple calls with 'AND'. + * + * @param string $key + * @param string $value + * @param bool $escape + * @return CI_DB_query_builder + */ + public function having($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_having', $key, $value, 'AND ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * OR HAVING + * + * Separates multiple calls with 'OR'. + * + * @param string $key + * @param string $value + * @param bool $escape + * @return CI_DB_query_builder + */ + public function or_having($key, $value = NULL, $escape = NULL) + { + return $this->_wh('qb_having', $key, $value, 'OR ', $escape); + } + + // -------------------------------------------------------------------- + + /** + * ORDER BY + * + * @param string $orderby + * @param string $direction ASC, DESC or RANDOM + * @param bool $escape + * @return CI_DB_query_builder + */ + public function order_by($orderby, $direction = '', $escape = NULL) + { + $direction = strtoupper(trim($direction)); + + if ($direction === 'RANDOM') + { + $direction = ''; + + // Do we have a seed value? + $orderby = ctype_digit((string) $orderby) + ? sprintf($this->_random_keyword[1], $orderby) + : $this->_random_keyword[0]; + } + elseif (empty($orderby)) + { + return $this; + } + elseif ($direction !== '') + { + $direction = in_array($direction, array('ASC', 'DESC'), TRUE) ? ' '.$direction : ''; + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + if ($escape === FALSE) + { + $qb_orderby[] = array('field' => $orderby, 'direction' => $direction, 'escape' => FALSE); + } + else + { + $qb_orderby = array(); + foreach (explode(',', $orderby) as $field) + { + $qb_orderby[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE)) + ? array('field' => ltrim(substr($field, 0, $match[0][1])), 'direction' => ' '.$match[1][0], 'escape' => TRUE) + : array('field' => trim($field), 'direction' => $direction, 'escape' => TRUE); + } + } + + $this->qb_orderby = array_merge($this->qb_orderby, $qb_orderby); + if ($this->qb_caching === TRUE) + { + $this->qb_cache_orderby = array_merge($this->qb_cache_orderby, $qb_orderby); + $this->qb_cache_exists[] = 'orderby'; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * @param int $value LIMIT value + * @param int $offset OFFSET value + * @return CI_DB_query_builder + */ + public function limit($value, $offset = 0) + { + is_null($value) OR $this->qb_limit = (int) $value; + empty($offset) OR $this->qb_offset = (int) $offset; + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Sets the OFFSET value + * + * @param int $offset OFFSET value + * @return CI_DB_query_builder + */ + public function offset($offset) + { + empty($offset) OR $this->qb_offset = (int) $offset; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * LIMIT string + * + * Generates a platform-specific LIMIT clause. + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.($this->qb_offset ? $this->qb_offset.', ' : '').(int) $this->qb_limit; + } + + // -------------------------------------------------------------------- + + /** + * The "set" function. + * + * Allows key/value pairs to be set for inserting or updating + * + * @param mixed + * @param string + * @param bool + * @return CI_DB_query_builder + */ + public function set($key, $value = '', $escape = NULL) + { + $key = $this->_object_to_array($key); + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + foreach ($key as $k => $v) + { + $this->qb_set[$this->protect_identifiers($k, FALSE, $escape)] = ($escape) + ? $this->escape($v) : $v; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get SELECT query string + * + * Compiles a SELECT query string and returns the sql. + * + * @param string the table name to select from (optional) + * @param bool TRUE: resets QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_select($table = '', $reset = TRUE) + { + if ($table !== '') + { + $this->_track_aliases($table); + $this->from($table); + } + + $select = $this->_compile_select(); + + if ($reset === TRUE) + { + $this->_reset_select(); + } + + return $select; + } + + // -------------------------------------------------------------------- + + /** + * Get + * + * Compiles the select statement based on the other functions called + * and runs the query + * + * @param string the table + * @param string the limit clause + * @param string the offset clause + * @return CI_DB_result + */ + public function get($table = '', $limit = NULL, $offset = NULL) + { + if ($table !== '') + { + $this->_track_aliases($table); + $this->from($table); + } + + if ( ! empty($limit)) + { + $this->limit($limit, $offset); + } + + $result = $this->query($this->_compile_select()); + $this->_reset_select(); + return $result; + } + + // -------------------------------------------------------------------- + + /** + * "Count All Results" query + * + * Generates a platform-specific query string that counts all records + * returned by an Query Builder query. + * + * @param string + * @param bool the reset clause + * @return int + */ + public function count_all_results($table = '', $reset = TRUE) + { + if ($table !== '') + { + $this->_track_aliases($table); + $this->from($table); + } + + // ORDER BY usage is often problematic here (most notably + // on Microsoft SQL Server) and ultimately unnecessary + // for selecting COUNT(*) ... + $qb_orderby = $this->qb_orderby; + $qb_cache_orderby = $this->qb_cache_orderby; + $this->qb_orderby = $this->qb_cache_orderby = array(); + + $result = ($this->qb_distinct === TRUE OR ! empty($this->qb_groupby) OR ! empty($this->qb_cache_groupby) OR ! empty($this->qb_having) OR $this->qb_limit OR $this->qb_offset) + ? $this->query($this->_count_string.$this->protect_identifiers('numrows')."\nFROM (\n".$this->_compile_select()."\n) CI_count_all_results") + : $this->query($this->_compile_select($this->_count_string.$this->protect_identifiers('numrows'))); + + if ($reset === TRUE) + { + $this->_reset_select(); + } + else + { + $this->qb_orderby = $qb_orderby; + $this->qb_cache_orderby = $qb_cache_orderby; + } + + if ($result->num_rows() === 0) + { + return 0; + } + + $row = $result->row(); + return (int) $row->numrows; + } + + // -------------------------------------------------------------------- + + /** + * get_where() + * + * Allows the where clause, limit and offset to be added directly + * + * @param string $table + * @param string $where + * @param int $limit + * @param int $offset + * @return CI_DB_result + */ + public function get_where($table = '', $where = NULL, $limit = NULL, $offset = NULL) + { + if ($table !== '') + { + $this->from($table); + } + + if ($where !== NULL) + { + $this->where($where); + } + + if ( ! empty($limit)) + { + $this->limit($limit, $offset); + } + + $result = $this->query($this->_compile_select()); + $this->_reset_select(); + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Insert_Batch + * + * Compiles batch insert strings and runs the queries + * + * @param string $table Table to insert into + * @param array $set An associative array of insert values + * @param bool $escape Whether to escape values and identifiers + * @return int Number of rows inserted or FALSE on failure + */ + public function insert_batch($table, $set = NULL, $escape = NULL, $batch_size = 100) + { + if ($set === NULL) + { + if (empty($this->qb_set)) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + } + else + { + if (empty($set)) + { + return ($this->db_debug) ? $this->display_error('insert_batch() called with no data') : FALSE; + } + + $this->set_insert_batch($set, '', $escape); + } + + if (strlen($table) === 0) + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + + // Batch this baby + $affected_rows = 0; + for ($i = 0, $total = count($this->qb_set); $i < $total; $i += $batch_size) + { + if ($this->query($this->_insert_batch($this->protect_identifiers($table, TRUE, $escape, FALSE), $this->qb_keys, array_slice($this->qb_set, $i, $batch_size)))) + { + $affected_rows += $this->affected_rows(); + } + } + + $this->_reset_write(); + return $affected_rows; + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _insert_batch($table, $keys, $values) + { + return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES '.implode(', ', $values); + } + + // -------------------------------------------------------------------- + + /** + * The "set_insert_batch" function. Allows key/value pairs to be set for batch inserts + * + * @param mixed + * @param string + * @param bool + * @return CI_DB_query_builder + */ + public function set_insert_batch($key, $value = '', $escape = NULL) + { + $key = $this->_object_to_array_batch($key); + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + $keys = array_keys($this->_object_to_array(reset($key))); + sort($keys); + + foreach ($key as $row) + { + $row = $this->_object_to_array($row); + if (count(array_diff($keys, array_keys($row))) > 0 OR count(array_diff(array_keys($row), $keys)) > 0) + { + // batch function above returns an error on an empty array + $this->qb_set[] = array(); + return; + } + + ksort($row); // puts $row in the same order as our keys + + if ($escape !== FALSE) + { + $clean = array(); + foreach ($row as $value) + { + $clean[] = $this->escape($value); + } + + $row = $clean; + } + + $this->qb_set[] = '('.implode(',', $row).')'; + } + + foreach ($keys as $k) + { + $this->qb_keys[] = $this->protect_identifiers($k, FALSE, $escape); + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Get INSERT query string + * + * Compiles an insert query and returns the sql + * + * @param string the table to insert into + * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_insert($table = '', $reset = TRUE) + { + if ($this->_validate_insert($table) === FALSE) + { + return FALSE; + } + + $sql = $this->_insert( + $this->protect_identifiers( + $this->qb_from[0], TRUE, NULL, FALSE + ), + array_keys($this->qb_set), + array_values($this->qb_set) + ); + + if ($reset === TRUE) + { + $this->_reset_write(); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Insert + * + * Compiles an insert string and runs the query + * + * @param string the table to insert data into + * @param array an associative array of insert values + * @param bool $escape Whether to escape values and identifiers + * @return bool TRUE on success, FALSE on failure + */ + public function insert($table = '', $set = NULL, $escape = NULL) + { + if ($set !== NULL) + { + $this->set($set, '', $escape); + } + + if ($this->_validate_insert($table) === FALSE) + { + return FALSE; + } + + $sql = $this->_insert( + $this->protect_identifiers( + $this->qb_from[0], TRUE, $escape, FALSE + ), + array_keys($this->qb_set), + array_values($this->qb_set) + ); + + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Validate Insert + * + * This method is used by both insert() and get_compiled_insert() to + * validate that the there data is actually being set and that table + * has been chosen to be inserted into. + * + * @param string the table to insert data into + * @return string + */ + protected function _validate_insert($table = '') + { + if (count($this->qb_set) === 0) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + + if ($table !== '') + { + $this->qb_from[0] = $table; + } + elseif ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Replace + * + * Compiles an replace into string and runs the query + * + * @param string the table to replace data into + * @param array an associative array of insert values + * @return bool TRUE on success, FALSE on failure + */ + public function replace($table = '', $set = NULL) + { + if ($set !== NULL) + { + $this->set($set); + } + + if (count($this->qb_set) === 0) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + + $sql = $this->_replace($this->protect_identifiers($table, TRUE, NULL, FALSE), array_keys($this->qb_set), array_values($this->qb_set)); + + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * Generates a platform-specific replace string from the supplied data + * + * @param string the table name + * @param array the insert keys + * @param array the insert values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'REPLACE INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * Note: This is only used (and overridden) by MySQL and CUBRID. + * + * @return string + */ + protected function _from_tables() + { + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Get UPDATE query string + * + * Compiles an update query and returns the sql + * + * @param string the table to update + * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_update($table = '', $reset = TRUE) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($this->_validate_update($table) === FALSE) + { + return FALSE; + } + + $sql = $this->_update($this->qb_from[0], $this->qb_set); + + if ($reset === TRUE) + { + $this->_reset_write(); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * UPDATE + * + * Compiles an update string and runs the query. + * + * @param string $table + * @param array $set An associative array of update values + * @param mixed $where + * @param int $limit + * @return bool TRUE on success, FALSE on failure + */ + public function update($table = '', $set = NULL, $where = NULL, $limit = NULL) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($set !== NULL) + { + $this->set($set); + } + + if ($this->_validate_update($table) === FALSE) + { + return FALSE; + } + + if ($where !== NULL) + { + $this->where($where); + } + + if ( ! empty($limit)) + { + $this->limit($limit); + } + + $sql = $this->_update($this->qb_from[0], $this->qb_set); + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Validate Update + * + * This method is used by both update() and get_compiled_update() to + * validate that data is actually being set and that a table has been + * chosen to be update. + * + * @param string the table to update data on + * @return bool + */ + protected function _validate_update($table) + { + if (count($this->qb_set) === 0) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + + if ($table !== '') + { + $this->qb_from = array($this->protect_identifiers($table, TRUE, NULL, FALSE)); + } + elseif ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch + * + * Compiles an update string and runs the query + * + * @param string the table to retrieve the results from + * @param array an associative array of update values + * @param string the where key + * @return int number of rows affected or FALSE on failure + */ + public function update_batch($table, $set = NULL, $index = NULL, $batch_size = 100) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($index === NULL) + { + return ($this->db_debug) ? $this->display_error('db_must_use_index') : FALSE; + } + + if ($set === NULL) + { + if (empty($this->qb_set_ub)) + { + return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE; + } + } + else + { + if (empty($set)) + { + return ($this->db_debug) ? $this->display_error('update_batch() called with no data') : FALSE; + } + + $this->set_update_batch($set, $index); + } + + if (strlen($table) === 0) + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + + // Batch this baby + $affected_rows = 0; + for ($i = 0, $total = count($this->qb_set_ub); $i < $total; $i += $batch_size) + { + if ($this->query($this->_update_batch($this->protect_identifiers($table, TRUE, NULL, FALSE), array_slice($this->qb_set_ub, $i, $batch_size), $index))) + { + $affected_rows += $this->affected_rows(); + } + + $this->qb_where = array(); + } + + $this->_reset_write(); + return $affected_rows; + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]['value']; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$val[$field]['field']][] = 'WHEN '.$val[$index]['field'].' = '.$val[$index]['value'].' THEN '.$val[$field]['value']; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k." = CASE \n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END, '; + } + + $this->where($val[$index]['field'].' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * The "set_update_batch" function. Allows key/value pairs to be set for batch updating + * + * @param array + * @param string + * @param bool + * @return CI_DB_query_builder + */ + public function set_update_batch($key, $index = '', $escape = NULL) + { + $key = $this->_object_to_array_batch($key); + + if ( ! is_array($key)) + { + // @todo error + } + + is_bool($escape) OR $escape = $this->_protect_identifiers; + + foreach ($key as $k => $v) + { + $index_set = FALSE; + $clean = array(); + foreach ($v as $k2 => $v2) + { + if ($k2 === $index) + { + $index_set = TRUE; + } + + $clean[$k2] = array( + 'field' => $this->protect_identifiers($k2, FALSE, $escape), + 'value' => ($escape === FALSE ? $v2 : $this->escape($v2)) + ); + } + + if ($index_set === FALSE) + { + return $this->display_error('db_batch_missing_index'); + } + + $this->qb_set_ub[] = $clean; + } + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Empty Table + * + * Compiles a delete string and runs "DELETE FROM table" + * + * @param string the table to empty + * @return bool TRUE on success, FALSE on failure + */ + public function empty_table($table = '') + { + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + else + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + $sql = $this->_delete($table); + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Truncate + * + * Compiles a truncate string and runs the query + * If the database does not support the truncate() command + * This function maps to "DELETE FROM table" + * + * @param string the table to truncate + * @return bool TRUE on success, FALSE on failure + */ + public function truncate($table = '') + { + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + else + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + $sql = $this->_truncate($table); + $this->_reset_write(); + return $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the truncate() command, + * then this method maps to 'DELETE FROM table' + * + * @param string the table name + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Get DELETE query string + * + * Compiles a delete query string and returns the sql + * + * @param string the table to delete from + * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @return string + */ + public function get_compiled_delete($table = '', $reset = TRUE) + { + $this->return_delete_sql = TRUE; + $sql = $this->delete($table, '', NULL, $reset); + $this->return_delete_sql = FALSE; + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Delete + * + * Compiles a delete string and runs the query + * + * @param mixed the table(s) to delete from. String or array + * @param mixed the where clause + * @param mixed the limit clause + * @param bool + * @return mixed + */ + public function delete($table = '', $where = '', $limit = NULL, $reset_data = TRUE) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + if ($table === '') + { + if ( ! isset($this->qb_from[0])) + { + return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE; + } + + $table = $this->qb_from[0]; + } + elseif (is_array($table)) + { + empty($where) && $reset_data = FALSE; + + foreach ($table as $single_table) + { + $this->delete($single_table, $where, $limit, $reset_data); + } + + return; + } + else + { + $table = $this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + if ($where !== '') + { + $this->where($where); + } + + if ( ! empty($limit)) + { + $this->limit($limit); + } + + if (count($this->qb_where) === 0) + { + return ($this->db_debug) ? $this->display_error('db_del_must_use_where') : FALSE; + } + + $sql = $this->_delete($table); + if ($reset_data) + { + $this->_reset_write(); + } + + return ($this->return_delete_sql === TRUE) ? $sql : $this->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string the table name + * @return string + */ + protected function _delete($table) + { + return 'DELETE FROM '.$table.$this->_compile_wh('qb_where') + .($this->qb_limit !== FALSE ? ' LIMIT '.$this->qb_limit : ''); + } + + // -------------------------------------------------------------------- + + /** + * DB Prefix + * + * Prepends a database prefix if one exists in configuration + * + * @param string the table + * @return string + */ + public function dbprefix($table = '') + { + if ($table === '') + { + $this->display_error('db_table_name_required'); + } + + return $this->dbprefix.$table; + } + + // -------------------------------------------------------------------- + + /** + * Set DB Prefix + * + * Set's the DB Prefix to something new without needing to reconnect + * + * @param string the prefix + * @return string + */ + public function set_dbprefix($prefix = '') + { + return $this->dbprefix = $prefix; + } + + // -------------------------------------------------------------------- + + /** + * Track Aliases + * + * Used to track SQL statements written with aliased tables. + * + * @param string The table to inspect + * @return string + */ + protected function _track_aliases($table) + { + if (is_array($table)) + { + foreach ($table as $t) + { + $this->_track_aliases($t); + } + return; + } + + // Does the string contain a comma? If so, we need to separate + // the string into discreet statements + if (strpos($table, ',') !== FALSE) + { + return $this->_track_aliases(explode(',', $table)); + } + + // if a table alias is used we can recognize it by a space + if (strpos($table, ' ') !== FALSE) + { + // if the alias is written with the AS keyword, remove it + $table = preg_replace('/\s+AS\s+/i', ' ', $table); + + // Grab the alias + $table = trim(strrchr($table, ' ')); + + // Store the alias, if it doesn't already exist + if ( ! in_array($table, $this->qb_aliased_tables, TRUE)) + { + $this->qb_aliased_tables[] = $table; + if ($this->qb_caching === TRUE && ! in_array($table, $this->qb_cache_aliased_tables, TRUE)) + { + $this->qb_cache_aliased_tables[] = $table; + $this->qb_cache_exists[] = 'aliased_tables'; + } + } + } + } + + // -------------------------------------------------------------------- + + /** + * Compile the SELECT statement + * + * Generates a query string based on which functions were used. + * Should not be called directly. + * + * @param bool $select_override + * @return string + */ + protected function _compile_select($select_override = FALSE) + { + // Combine any cached components with the current statements + $this->_merge_cache(); + + // Write the "select" portion of the query + if ($select_override !== FALSE) + { + $sql = $select_override; + } + else + { + $sql = ( ! $this->qb_distinct) ? 'SELECT ' : 'SELECT DISTINCT '; + + if (count($this->qb_select) === 0) + { + $sql .= '*'; + } + else + { + // Cycle through the "select" portion of the query and prep each column name. + // The reason we protect identifiers here rather than in the select() function + // is because until the user calls the from() function we don't know if there are aliases + foreach ($this->qb_select as $key => $val) + { + $no_escape = isset($this->qb_no_escape[$key]) ? $this->qb_no_escape[$key] : NULL; + $this->qb_select[$key] = $this->protect_identifiers($val, FALSE, $no_escape); + } + + $sql .= implode(', ', $this->qb_select); + } + } + + // Write the "FROM" portion of the query + if (count($this->qb_from) > 0) + { + $sql .= "\nFROM ".$this->_from_tables(); + } + + // Write the "JOIN" portion of the query + if (count($this->qb_join) > 0) + { + $sql .= "\n".implode("\n", $this->qb_join); + } + + $sql .= $this->_compile_wh('qb_where') + .$this->_compile_group_by() + .$this->_compile_wh('qb_having') + .$this->_compile_order_by(); // ORDER BY + + // LIMIT + if ($this->qb_limit !== FALSE OR $this->qb_offset) + { + return $this->_limit($sql."\n"); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Compile WHERE, HAVING statements + * + * Escapes identifiers in WHERE and HAVING statements at execution time. + * + * Required so that aliases are tracked properly, regardless of whether + * where(), or_where(), having(), or_having are called prior to from(), + * join() and dbprefix is added only if needed. + * + * @param string $qb_key 'qb_where' or 'qb_having' + * @return string SQL statement + */ + protected function _compile_wh($qb_key) + { + if (count($this->$qb_key) > 0) + { + for ($i = 0, $c = count($this->$qb_key); $i < $c; $i++) + { + // Is this condition already compiled? + if (is_string($this->{$qb_key}[$i])) + { + continue; + } + elseif ($this->{$qb_key}[$i]['escape'] === FALSE) + { + $this->{$qb_key}[$i] = $this->{$qb_key}[$i]['condition'].(isset($this->{$qb_key}[$i]['value']) ? ' '.$this->{$qb_key}[$i]['value'] : ''); + continue; + } + + // Split multiple conditions + $conditions = preg_split( + '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i', + $this->{$qb_key}[$i]['condition'], + -1, + PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY + ); + + for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci++) + { + if (($op = $this->_get_operator($conditions[$ci])) === FALSE + OR ! preg_match('/^(\(?)(.*)('.preg_quote($op, '/').')\s*(.*(? '(test <= foo)', /* the whole thing */ + // 1 => '(', /* optional */ + // 2 => 'test', /* the field name */ + // 3 => ' <= ', /* $op */ + // 4 => 'foo', /* optional, if $op is e.g. 'IS NULL' */ + // 5 => ')' /* optional */ + // ); + + if ( ! empty($matches[4])) + { + $this->_is_literal($matches[4]) OR $matches[4] = $this->protect_identifiers(trim($matches[4])); + $matches[4] = ' '.$matches[4]; + } + + $conditions[$ci] = $matches[1].$this->protect_identifiers(trim($matches[2])) + .' '.trim($matches[3]).$matches[4].$matches[5]; + } + + $this->{$qb_key}[$i] = implode('', $conditions).(isset($this->{$qb_key}[$i]['value']) ? ' '.$this->{$qb_key}[$i]['value'] : ''); + } + + return ($qb_key === 'qb_having' ? "\nHAVING " : "\nWHERE ") + .implode("\n", $this->$qb_key); + } + + return ''; + } + + // -------------------------------------------------------------------- + + /** + * Compile GROUP BY + * + * Escapes identifiers in GROUP BY statements at execution time. + * + * Required so that aliases are tracked properly, regardless of whether + * group_by() is called prior to from(), join() and dbprefix is added + * only if needed. + * + * @return string SQL statement + */ + protected function _compile_group_by() + { + if (count($this->qb_groupby) > 0) + { + for ($i = 0, $c = count($this->qb_groupby); $i < $c; $i++) + { + // Is it already compiled? + if (is_string($this->qb_groupby[$i])) + { + continue; + } + + $this->qb_groupby[$i] = ($this->qb_groupby[$i]['escape'] === FALSE OR $this->_is_literal($this->qb_groupby[$i]['field'])) + ? $this->qb_groupby[$i]['field'] + : $this->protect_identifiers($this->qb_groupby[$i]['field']); + } + + return "\nGROUP BY ".implode(', ', $this->qb_groupby); + } + + return ''; + } + + // -------------------------------------------------------------------- + + /** + * Compile ORDER BY + * + * Escapes identifiers in ORDER BY statements at execution time. + * + * Required so that aliases are tracked properly, regardless of whether + * order_by() is called prior to from(), join() and dbprefix is added + * only if needed. + * + * @return string SQL statement + */ + protected function _compile_order_by() + { + if (empty($this->qb_orderby)) + { + return ''; + } + + for ($i = 0, $c = count($this->qb_orderby); $i < $c; $i++) + { + if (is_string($this->qb_orderby[$i])) + { + continue; + } + + if ($this->qb_orderby[$i]['escape'] !== FALSE && ! $this->_is_literal($this->qb_orderby[$i]['field'])) + { + $this->qb_orderby[$i]['field'] = $this->protect_identifiers($this->qb_orderby[$i]['field']); + } + + $this->qb_orderby[$i] = $this->qb_orderby[$i]['field'].$this->qb_orderby[$i]['direction']; + } + + return "\nORDER BY ".implode(', ', $this->qb_orderby); + } + + // -------------------------------------------------------------------- + + /** + * Object to Array + * + * Takes an object as input and converts the class variables to array key/vals + * + * @param object + * @return array + */ + protected function _object_to_array($object) + { + if ( ! is_object($object)) + { + return $object; + } + + $array = array(); + foreach (get_object_vars($object) as $key => $val) + { + // There are some built in keys we need to ignore for this conversion + if ( ! is_object($val) && ! is_array($val) && $key !== '_parent_name') + { + $array[$key] = $val; + } + } + + return $array; + } + + // -------------------------------------------------------------------- + + /** + * Object to Array + * + * Takes an object as input and converts the class variables to array key/vals + * + * @param object + * @return array + */ + protected function _object_to_array_batch($object) + { + if ( ! is_object($object)) + { + return $object; + } + + $array = array(); + $out = get_object_vars($object); + $fields = array_keys($out); + + foreach ($fields as $val) + { + // There are some built in keys we need to ignore for this conversion + if ($val !== '_parent_name') + { + $i = 0; + foreach ($out[$val] as $data) + { + $array[$i++][$val] = $data; + } + } + } + + return $array; + } + + // -------------------------------------------------------------------- + + /** + * Start Cache + * + * Starts QB caching + * + * @return CI_DB_query_builder + */ + public function start_cache() + { + $this->qb_caching = TRUE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Stop Cache + * + * Stops QB caching + * + * @return CI_DB_query_builder + */ + public function stop_cache() + { + $this->qb_caching = FALSE; + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Flush Cache + * + * Empties the QB cache + * + * @return CI_DB_query_builder + */ + public function flush_cache() + { + $this->_reset_run(array( + 'qb_cache_select' => array(), + 'qb_cache_from' => array(), + 'qb_cache_join' => array(), + 'qb_cache_where' => array(), + 'qb_cache_groupby' => array(), + 'qb_cache_having' => array(), + 'qb_cache_orderby' => array(), + 'qb_cache_set' => array(), + 'qb_cache_exists' => array(), + 'qb_cache_no_escape' => array(), + 'qb_cache_aliased_tables' => array() + )); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Merge Cache + * + * When called, this function merges any cached QB arrays with + * locally called ones. + * + * @return void + */ + protected function _merge_cache() + { + if (count($this->qb_cache_exists) === 0) + { + return; + } + elseif (in_array('select', $this->qb_cache_exists, TRUE)) + { + $qb_no_escape = $this->qb_cache_no_escape; + } + + foreach (array_unique($this->qb_cache_exists) as $val) // select, from, etc. + { + $qb_variable = 'qb_'.$val; + $qb_cache_var = 'qb_cache_'.$val; + $qb_new = $this->$qb_cache_var; + + for ($i = 0, $c = count($this->$qb_variable); $i < $c; $i++) + { + if ( ! in_array($this->{$qb_variable}[$i], $qb_new, TRUE)) + { + $qb_new[] = $this->{$qb_variable}[$i]; + if ($val === 'select') + { + $qb_no_escape[] = $this->qb_no_escape[$i]; + } + } + } + + $this->$qb_variable = $qb_new; + if ($val === 'select') + { + $this->qb_no_escape = $qb_no_escape; + } + } + } + + // -------------------------------------------------------------------- + + /** + * Is literal + * + * Determines if a string represents a literal value or a field name + * + * @param string $str + * @return bool + */ + protected function _is_literal($str) + { + $str = trim($str); + + if (empty($str) OR ctype_digit($str) OR (string) (float) $str === $str OR in_array(strtoupper($str), array('TRUE', 'FALSE'), TRUE)) + { + return TRUE; + } + + static $_str; + + if (empty($_str)) + { + $_str = ($this->_escape_char !== '"') + ? array('"', "'") : array("'"); + } + + return in_array($str[0], $_str, TRUE); + } + + // -------------------------------------------------------------------- + + /** + * Reset Query Builder values. + * + * Publicly-visible method to reset the QB values. + * + * @return CI_DB_query_builder + */ + public function reset_query() + { + $this->_reset_select(); + $this->_reset_write(); + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Resets the query builder values. Called by the get() function + * + * @param array An array of fields to reset + * @return void + */ + protected function _reset_run($qb_reset_items) + { + foreach ($qb_reset_items as $item => $default_value) + { + $this->$item = $default_value; + } + } + + // -------------------------------------------------------------------- + + /** + * Resets the query builder values. Called by the get() function + * + * @return void + */ + protected function _reset_select() + { + $this->_reset_run(array( + 'qb_select' => array(), + 'qb_from' => array(), + 'qb_join' => array(), + 'qb_where' => array(), + 'qb_groupby' => array(), + 'qb_having' => array(), + 'qb_orderby' => array(), + 'qb_aliased_tables' => array(), + 'qb_no_escape' => array(), + 'qb_distinct' => FALSE, + 'qb_limit' => FALSE, + 'qb_offset' => FALSE + )); + } + + // -------------------------------------------------------------------- + + /** + * Resets the query builder "write" values. + * + * Called by the insert() update() insert_batch() update_batch() and delete() functions + * + * @return void + */ + protected function _reset_write() + { + $this->_reset_run(array( + 'qb_set' => array(), + 'qb_set_ub' => array(), + 'qb_from' => array(), + 'qb_join' => array(), + 'qb_where' => array(), + 'qb_orderby' => array(), + 'qb_keys' => array(), + 'qb_limit' => FALSE + )); + } + +} diff --git a/system/database/DB_result.php b/system/database/DB_result.php new file mode 100644 index 0000000..94da294 --- /dev/null +++ b/system/database/DB_result.php @@ -0,0 +1,666 @@ +conn_id = $driver_object->conn_id; + $this->result_id = $driver_object->result_id; + } + + // -------------------------------------------------------------------- + + /** + * Number of rows in the result set + * + * @return int + */ + public function num_rows() + { + if (is_int($this->num_rows)) + { + return $this->num_rows; + } + elseif (count($this->result_array) > 0) + { + return $this->num_rows = count($this->result_array); + } + elseif (count($this->result_object) > 0) + { + return $this->num_rows = count($this->result_object); + } + + return $this->num_rows = count($this->result_array()); + } + + // -------------------------------------------------------------------- + + /** + * Query result. Acts as a wrapper function for the following functions. + * + * @param string $type 'object', 'array' or a custom class name + * @return array + */ + public function result($type = 'object') + { + if ($type === 'array') + { + return $this->result_array(); + } + elseif ($type === 'object') + { + return $this->result_object(); + } + + return $this->custom_result_object($type); + } + + // -------------------------------------------------------------------- + + /** + * Custom query result. + * + * @param string $class_name + * @return array + */ + public function custom_result_object($class_name) + { + if (isset($this->custom_result_object[$class_name])) + { + return $this->custom_result_object[$class_name]; + } + elseif ( ! $this->result_id OR $this->num_rows === 0) + { + return array(); + } + + // Don't fetch the result set again if we already have it + $_data = NULL; + if (($c = count($this->result_array)) > 0) + { + $_data = 'result_array'; + } + elseif (($c = count($this->result_object)) > 0) + { + $_data = 'result_object'; + } + + if ($_data !== NULL) + { + for ($i = 0; $i < $c; $i++) + { + $this->custom_result_object[$class_name][$i] = new $class_name(); + + foreach ($this->{$_data}[$i] as $key => $value) + { + $this->custom_result_object[$class_name][$i]->$key = $value; + } + } + + return $this->custom_result_object[$class_name]; + } + + is_null($this->row_data) OR $this->data_seek(0); + $this->custom_result_object[$class_name] = array(); + + while ($row = $this->_fetch_object($class_name)) + { + $this->custom_result_object[$class_name][] = $row; + } + + return $this->custom_result_object[$class_name]; + } + + // -------------------------------------------------------------------- + + /** + * Query result. "object" version. + * + * @return array + */ + public function result_object() + { + if (count($this->result_object) > 0) + { + return $this->result_object; + } + + // In the event that query caching is on, the result_id variable + // will not be a valid resource so we'll simply return an empty + // array. + if ( ! $this->result_id OR $this->num_rows === 0) + { + return array(); + } + + if (($c = count($this->result_array)) > 0) + { + for ($i = 0; $i < $c; $i++) + { + $this->result_object[$i] = (object) $this->result_array[$i]; + } + + return $this->result_object; + } + + is_null($this->row_data) OR $this->data_seek(0); + while ($row = $this->_fetch_object()) + { + $this->result_object[] = $row; + } + + return $this->result_object; + } + + // -------------------------------------------------------------------- + + /** + * Query result. "array" version. + * + * @return array + */ + public function result_array() + { + if (count($this->result_array) > 0) + { + return $this->result_array; + } + + // In the event that query caching is on, the result_id variable + // will not be a valid resource so we'll simply return an empty + // array. + if ( ! $this->result_id OR $this->num_rows === 0) + { + return array(); + } + + if (($c = count($this->result_object)) > 0) + { + for ($i = 0; $i < $c; $i++) + { + $this->result_array[$i] = (array) $this->result_object[$i]; + } + + return $this->result_array; + } + + is_null($this->row_data) OR $this->data_seek(0); + while ($row = $this->_fetch_assoc()) + { + $this->result_array[] = $row; + } + + return $this->result_array; + } + + // -------------------------------------------------------------------- + + /** + * Row + * + * A wrapper method. + * + * @param mixed $n + * @param string $type 'object' or 'array' + * @return mixed + */ + public function row($n = 0, $type = 'object') + { + if ( ! is_numeric($n)) + { + // We cache the row data for subsequent uses + is_array($this->row_data) OR $this->row_data = $this->row_array(0); + + // array_key_exists() instead of isset() to allow for NULL values + if (empty($this->row_data) OR ! array_key_exists($n, $this->row_data)) + { + return NULL; + } + + return $this->row_data[$n]; + } + + if ($type === 'object') return $this->row_object($n); + elseif ($type === 'array') return $this->row_array($n); + + return $this->custom_row_object($n, $type); + } + + // -------------------------------------------------------------------- + + /** + * Assigns an item into a particular column slot + * + * @param mixed $key + * @param mixed $value + * @return void + */ + public function set_row($key, $value = NULL) + { + // We cache the row data for subsequent uses + if ( ! is_array($this->row_data)) + { + $this->row_data = $this->row_array(0); + } + + if (is_array($key)) + { + foreach ($key as $k => $v) + { + $this->row_data[$k] = $v; + } + return; + } + + if ($key !== '' && $value !== NULL) + { + $this->row_data[$key] = $value; + } + } + + // -------------------------------------------------------------------- + + /** + * Returns a single result row - custom object version + * + * @param int $n + * @param string $type + * @return object + */ + public function custom_row_object($n, $type) + { + isset($this->custom_result_object[$type]) OR $this->custom_result_object[$type] = $this->custom_result_object($type); + + if (count($this->custom_result_object[$type]) === 0) + { + return NULL; + } + + if ($n !== $this->current_row && isset($this->custom_result_object[$type][$n])) + { + $this->current_row = $n; + } + + return $this->custom_result_object[$type][$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns a single result row - object version + * + * @param int $n + * @return object + */ + public function row_object($n = 0) + { + $result = $this->result_object(); + if (count($result) === 0) + { + return NULL; + } + + if ($n !== $this->current_row && isset($result[$n])) + { + $this->current_row = $n; + } + + return $result[$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns a single result row - array version + * + * @param int $n + * @return array + */ + public function row_array($n = 0) + { + $result = $this->result_array(); + if (count($result) === 0) + { + return NULL; + } + + if ($n !== $this->current_row && isset($result[$n])) + { + $this->current_row = $n; + } + + return $result[$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "first" row + * + * @param string $type + * @return mixed + */ + public function first_row($type = 'object') + { + $result = $this->result($type); + return (count($result) === 0) ? NULL : $result[0]; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "last" row + * + * @param string $type + * @return mixed + */ + public function last_row($type = 'object') + { + $result = $this->result($type); + return (count($result) === 0) ? NULL : $result[count($result) - 1]; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "next" row + * + * @param string $type + * @return mixed + */ + public function next_row($type = 'object') + { + $result = $this->result($type); + if (count($result) === 0) + { + return NULL; + } + + return isset($result[$this->current_row + 1]) + ? $result[++$this->current_row] + : NULL; + } + + // -------------------------------------------------------------------- + + /** + * Returns the "previous" row + * + * @param string $type + * @return mixed + */ + public function previous_row($type = 'object') + { + $result = $this->result($type); + if (count($result) === 0) + { + return NULL; + } + + if (isset($result[$this->current_row - 1])) + { + --$this->current_row; + } + return $result[$this->current_row]; + } + + // -------------------------------------------------------------------- + + /** + * Returns an unbuffered row and move pointer to next row + * + * @param string $type 'array', 'object' or a custom class name + * @return mixed + */ + public function unbuffered_row($type = 'object') + { + if ($type === 'array') + { + return $this->_fetch_assoc(); + } + elseif ($type === 'object') + { + return $this->_fetch_object(); + } + + return $this->_fetch_object($type); + } + + // -------------------------------------------------------------------- + + /** + * The following methods are normally overloaded by the identically named + * methods in the platform-specific driver -- except when query caching + * is used. When caching is enabled we do not load the other driver. + * These functions are primarily here to prevent undefined function errors + * when a cached result object is in use. They are not otherwise fully + * operational due to the unavailability of the database resource IDs with + * cached results. + */ + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * Overridden by driver result classes. + * + * @return int + */ + public function num_fields() + { + return 0; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names. + * + * Overridden by driver result classes. + * + * @return array + */ + public function list_fields() + { + return array(); + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data. + * + * Overridden by driver result classes. + * + * @return array + */ + public function field_data() + { + return array(); + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * Overridden by driver result classes. + * + * @return void + */ + public function free_result() + { + $this->result_id = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * Overridden by driver result classes. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array. + * + * Overridden by driver result classes. + * + * @return array + */ + protected function _fetch_assoc() + { + return array(); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object. + * + * Overridden by driver result classes. + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return new $class_name(); + } + +} diff --git a/system/database/DB_utility.php b/system/database/DB_utility.php new file mode 100644 index 0000000..11aa67b --- /dev/null +++ b/system/database/DB_utility.php @@ -0,0 +1,425 @@ +db =& $db; + log_message('info', 'Database Utility Class Initialized'); + } + + // -------------------------------------------------------------------- + + /** + * List databases + * + * @return array + */ + public function list_databases() + { + // Is there a cached result? + if (isset($this->db->data_cache['db_names'])) + { + return $this->db->data_cache['db_names']; + } + elseif ($this->_list_databases === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $this->db->data_cache['db_names'] = array(); + + $query = $this->db->query($this->_list_databases); + if ($query === FALSE) + { + return $this->db->data_cache['db_names']; + } + + for ($i = 0, $query = $query->result_array(), $c = count($query); $i < $c; $i++) + { + $this->db->data_cache['db_names'][] = current($query[$i]); + } + + return $this->db->data_cache['db_names']; + } + + // -------------------------------------------------------------------- + + /** + * Determine if a particular database exists + * + * @param string $database_name + * @return bool + */ + public function database_exists($database_name) + { + return in_array($database_name, $this->list_databases()); + } + + // -------------------------------------------------------------------- + + /** + * Optimize Table + * + * @param string $table_name + * @return mixed + */ + public function optimize_table($table_name) + { + if ($this->_optimize_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $query = $this->db->query(sprintf($this->_optimize_table, $this->db->escape_identifiers($table_name))); + if ($query !== FALSE) + { + $query = $query->result_array(); + return current($query); + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Optimize Database + * + * @return mixed + */ + public function optimize_database() + { + if ($this->_optimize_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $result = array(); + foreach ($this->db->list_tables() as $table_name) + { + $res = $this->db->query(sprintf($this->_optimize_table, $this->db->escape_identifiers($table_name))); + if (is_bool($res)) + { + return $res; + } + + // Build the result array... + $res = $res->result_array(); + $res = current($res); + $key = str_replace($this->db->database.'.', '', current($res)); + $keys = array_keys($res); + unset($res[$keys[0]]); + + $result[$key] = $res; + } + + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Repair Table + * + * @param string $table_name + * @return mixed + */ + public function repair_table($table_name) + { + if ($this->_repair_table === FALSE) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE; + } + + $query = $this->db->query(sprintf($this->_repair_table, $this->db->escape_identifiers($table_name))); + if (is_bool($query)) + { + return $query; + } + + $query = $query->result_array(); + return current($query); + } + + // -------------------------------------------------------------------- + + /** + * Generate CSV from a query result object + * + * @param object $query Query result object + * @param string $delim Delimiter (default: ,) + * @param string $newline Newline character (default: \n) + * @param string $enclosure Enclosure (default: ") + * @return string + */ + public function csv_from_result($query, $delim = ',', $newline = "\n", $enclosure = '"') + { + if ( ! is_object($query) OR ! method_exists($query, 'list_fields')) + { + show_error('You must submit a valid result object'); + } + + $out = ''; + // First generate the headings from the table column names + foreach ($query->list_fields() as $name) + { + $out .= $enclosure.str_replace($enclosure, $enclosure.$enclosure, $name).$enclosure.$delim; + } + + $out = substr($out, 0, -strlen($delim)).$newline; + + // Next blast through the result array and build out the rows + while ($row = $query->unbuffered_row('array')) + { + $line = array(); + foreach ($row as $item) + { + $line[] = $enclosure.str_replace($enclosure, $enclosure.$enclosure, (string) $item).$enclosure; + } + $out .= implode($delim, $line).$newline; + } + + return $out; + } + + // -------------------------------------------------------------------- + + /** + * Generate XML data from a query result object + * + * @param object $query Query result object + * @param array $params Any preferences + * @return string + */ + public function xml_from_result($query, $params = array()) + { + if ( ! is_object($query) OR ! method_exists($query, 'list_fields')) + { + show_error('You must submit a valid result object'); + } + + // Set our default values + foreach (array('root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t") as $key => $val) + { + if ( ! isset($params[$key])) + { + $params[$key] = $val; + } + } + + // Create variables for convenience + extract($params); + + // Load the xml helper + get_instance()->load->helper('xml'); + + // Generate the result + $xml = '<'.$root.'>'.$newline; + while ($row = $query->unbuffered_row()) + { + $xml .= $tab.'<'.$element.'>'.$newline; + foreach ($row as $key => $val) + { + $xml .= $tab.$tab.'<'.$key.'>'.xml_convert($val).''.$newline; + } + $xml .= $tab.''.$newline; + } + + return $xml.''.$newline; + } + + // -------------------------------------------------------------------- + + /** + * Database Backup + * + * @param array $params + * @return string + */ + public function backup($params = array()) + { + // If the parameters have not been submitted as an + // array then we know that it is simply the table + // name, which is a valid short cut. + if (is_string($params)) + { + $params = array('tables' => $params); + } + + // Set up our default preferences + $prefs = array( + 'tables' => array(), + 'ignore' => array(), + 'filename' => '', + 'format' => 'gzip', // gzip, zip, txt + 'add_drop' => TRUE, + 'add_insert' => TRUE, + 'newline' => "\n", + 'foreign_key_checks' => TRUE + ); + + // Did the user submit any preferences? If so set them.... + if (count($params) > 0) + { + foreach ($prefs as $key => $val) + { + if (isset($params[$key])) + { + $prefs[$key] = $params[$key]; + } + } + } + + // Are we backing up a complete database or individual tables? + // If no table names were submitted we'll fetch the entire table list + if (count($prefs['tables']) === 0) + { + $prefs['tables'] = $this->db->list_tables(); + } + + // Validate the format + if ( ! in_array($prefs['format'], array('gzip', 'zip', 'txt'), TRUE)) + { + $prefs['format'] = 'txt'; + } + + // Is the encoder supported? If not, we'll either issue an + // error or use plain text depending on the debug settings + if (($prefs['format'] === 'gzip' && ! function_exists('gzencode')) + OR ($prefs['format'] === 'zip' && ! function_exists('gzcompress'))) + { + if ($this->db->db_debug) + { + return $this->db->display_error('db_unsupported_compression'); + } + + $prefs['format'] = 'txt'; + } + + // Was a Zip file requested? + if ($prefs['format'] === 'zip') + { + // Set the filename if not provided (only needed with Zip files) + if ($prefs['filename'] === '') + { + $prefs['filename'] = (count($prefs['tables']) === 1 ? $prefs['tables'] : $this->db->database) + .date('Y-m-d_H-i', time()).'.sql'; + } + else + { + // If they included the .zip file extension we'll remove it + if (preg_match('|.+?\.zip$|', $prefs['filename'])) + { + $prefs['filename'] = str_replace('.zip', '', $prefs['filename']); + } + + // Tack on the ".sql" file extension if needed + if ( ! preg_match('|.+?\.sql$|', $prefs['filename'])) + { + $prefs['filename'] .= '.sql'; + } + } + + // Load the Zip class and output it + $CI =& get_instance(); + $CI->load->library('zip'); + $CI->zip->add_data($prefs['filename'], $this->_backup($prefs)); + return $CI->zip->get_zip(); + } + elseif ($prefs['format'] === 'txt') // Was a text file requested? + { + return $this->_backup($prefs); + } + elseif ($prefs['format'] === 'gzip') // Was a Gzip file requested? + { + return gzencode($this->_backup($prefs)); + } + + return; + } + +} diff --git a/system/database/drivers/cubrid/cubrid_driver.php b/system/database/drivers/cubrid/cubrid_driver.php new file mode 100644 index 0000000..bd01be6 --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_driver.php @@ -0,0 +1,406 @@ +dsn, $matches)) + { + if (stripos($matches[2], 'autocommit=off') !== FALSE) + { + $this->auto_commit = FALSE; + } + } + else + { + // If no port is defined by the user, use the default value + empty($this->port) OR $this->port = 33000; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + if (preg_match('/^CUBRID:[^:]+(:[0-9][1-9]{0,4})?:[^:]+:([^:]*):([^:]*):(\?.+)?$/', $this->dsn, $matches)) + { + $func = ($persistent !== TRUE) ? 'cubrid_connect_with_url' : 'cubrid_pconnect_with_url'; + return ($matches[2] === '' && $matches[3] === '' && $this->username !== '' && $this->password !== '') + ? $func($this->dsn, $this->username, $this->password) + : $func($this->dsn); + } + + $func = ($persistent !== TRUE) ? 'cubrid_connect' : 'cubrid_pconnect'; + return ($this->username !== '') + ? $func($this->hostname, $this->port, $this->database, $this->username, $this->password) + : $func($this->hostname, $this->port, $this->database); + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if (cubrid_ping($this->conn_id) === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + return ( ! $this->conn_id OR ($version = cubrid_get_server_info($this->conn_id)) === FALSE) + ? FALSE + : $this->data_cache['version'] = $version; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return cubrid_query($sql, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + if (($autocommit = cubrid_get_autocommit($this->conn_id)) === NULL) + { + return FALSE; + } + elseif ($autocommit === TRUE) + { + return cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_FALSE); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ( ! cubrid_commit($this->conn_id)) + { + return FALSE; + } + + if ($this->auto_commit && ! cubrid_get_autocommit($this->conn_id)) + { + return cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_TRUE); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ( ! cubrid_rollback($this->conn_id)) + { + return FALSE; + } + + if ($this->auto_commit && ! cubrid_get_autocommit($this->conn_id)) + { + cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_TRUE); + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return cubrid_real_escape_string($str, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return cubrid_affected_rows(); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return cubrid_insert_id($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES'; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + return array('code' => cubrid_errno($this->conn_id), 'message' => cubrid_error($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + cubrid_close($this->conn_id); + } + +} diff --git a/system/database/drivers/cubrid/cubrid_forge.php b/system/database/drivers/cubrid/cubrid_forge.php new file mode 100644 index 0000000..e8e201f --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_forge.php @@ -0,0 +1,231 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'BIGINT' => 'NUMERIC', + 'FLOAT' => 'DOUBLE', + 'REAL' => 'DOUBLE' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $sqls[] = $sql.' CHANGE '.$field[$i]['_literal']; + } + else + { + $alter_type = empty($field[$i]['new_name']) ? ' MODIFY ' : ' CHANGE '; + $sqls[] = $sql.$alter_type.$this->_process_column($field[$i]); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'LONGTEXT': + $attributes['TYPE'] = 'STRING'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/cubrid/cubrid_result.php b/system/database/drivers/cubrid/cubrid_result.php new file mode 100644 index 0000000..274b0c9 --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_result.php @@ -0,0 +1,178 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = cubrid_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return cubrid_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + return cubrid_column_names($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = cubrid_field_name($this->result_id, $i); + $retval[$i]->type = cubrid_field_type($this->result_id, $i); + $retval[$i]->max_length = cubrid_field_len($this->result_id, $i); + $retval[$i]->primary_key = (int) (strpos(cubrid_field_flags($this->result_id, $i), 'primary_key') !== FALSE); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id) OR + (get_resource_type($this->result_id) === 'Unknown' && preg_match('/Resource id #/', strval($this->result_id)))) + { + cubrid_close_request($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return cubrid_data_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return cubrid_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return cubrid_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/cubrid/cubrid_utility.php b/system/database/drivers/cubrid/cubrid_utility.php new file mode 100644 index 0000000..ca81568 --- /dev/null +++ b/system/database/drivers/cubrid/cubrid_utility.php @@ -0,0 +1,80 @@ +db->data_cache['db_names'])) + { + return $this->db->data_cache['db_names']; + } + + return $this->db->data_cache['db_names'] = cubrid_list_dbs($this->db->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * CUBRID Export + * + * @param array Preferences + * @return mixed + */ + protected function _backup($params = array()) + { + // No SQL based support in CUBRID as of version 8.4.0. Database or + // table backup can be performed using CUBRID Manager + // database administration tool. + return $this->db->display_error('db_unsupported_feature'); + } +} diff --git a/system/database/drivers/cubrid/index.html b/system/database/drivers/cubrid/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/cubrid/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/ibase/ibase_driver.php b/system/database/drivers/ibase/ibase_driver.php new file mode 100644 index 0000000..433139f --- /dev/null +++ b/system/database/drivers/ibase/ibase_driver.php @@ -0,0 +1,414 @@ +hostname.':'.$this->database, $this->username, $this->password, $this->char_set) + : ibase_connect($this->hostname.':'.$this->database, $this->username, $this->password, $this->char_set); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if (($service = ibase_service_attach($this->hostname, $this->username, $this->password))) + { + $this->data_cache['version'] = ibase_server_info($service, IBASE_SVC_SERVER_VERSION); + + // Don't keep the service open + ibase_service_detach($service); + return $this->data_cache['version']; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return ibase_query(isset($this->_ibase_trans) ? $this->_ibase_trans : $this->conn_id, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + if (($trans_handle = ibase_trans($this->conn_id)) === FALSE) + { + return FALSE; + } + + $this->_ibase_trans = $trans_handle; + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if (ibase_commit($this->_ibase_trans)) + { + $this->_ibase_trans = NULL; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if (ibase_rollback($this->_ibase_trans)) + { + $this->_ibase_trans = NULL; + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return ibase_affected_rows($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @param string $generator_name + * @param int $inc_by + * @return int + */ + public function insert_id($generator_name, $inc_by = 0) + { + //If a generator hasn't been used before it will return 0 + return ibase_gen_id('"'.$generator_name.'"', $inc_by); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT TRIM("RDB$RELATION_NAME") AS TABLE_NAME FROM "RDB$RELATIONS" WHERE "RDB$RELATION_NAME" NOT LIKE \'RDB$%\' AND "RDB$RELATION_NAME" NOT LIKE \'MON$%\''; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql.' AND TRIM("RDB$RELATION_NAME") AS TABLE_NAME LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT TRIM("RDB$FIELD_NAME") AS COLUMN_NAME FROM "RDB$RELATION_FIELDS" WHERE "RDB$RELATION_NAME" = '.$this->escape($table); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "rfields"."RDB$FIELD_NAME" AS "name", + CASE "fields"."RDB$FIELD_TYPE" + WHEN 7 THEN \'SMALLINT\' + WHEN 8 THEN \'INTEGER\' + WHEN 9 THEN \'QUAD\' + WHEN 10 THEN \'FLOAT\' + WHEN 11 THEN \'DFLOAT\' + WHEN 12 THEN \'DATE\' + WHEN 13 THEN \'TIME\' + WHEN 14 THEN \'CHAR\' + WHEN 16 THEN \'INT64\' + WHEN 27 THEN \'DOUBLE\' + WHEN 35 THEN \'TIMESTAMP\' + WHEN 37 THEN \'VARCHAR\' + WHEN 40 THEN \'CSTRING\' + WHEN 261 THEN \'BLOB\' + ELSE NULL + END AS "type", + "fields"."RDB$FIELD_LENGTH" AS "max_length", + "rfields"."RDB$DEFAULT_VALUE" AS "default" + FROM "RDB$RELATION_FIELDS" "rfields" + JOIN "RDB$FIELDS" "fields" ON "rfields"."RDB$FIELD_SOURCE" = "fields"."RDB$FIELD_NAME" + WHERE "rfields"."RDB$RELATION_NAME" = '.$this->escape($table).' + ORDER BY "rfields"."RDB$FIELD_POSITION"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + return array('code' => ibase_errcode(), 'message' => ibase_errmsg()); + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // Limit clause depends on if Interbase or Firebird + if (stripos($this->version(), 'firebird') !== FALSE) + { + $select = 'FIRST '.$this->qb_limit + .($this->qb_offset ? ' SKIP '.$this->qb_offset : ''); + } + else + { + $select = 'ROWS ' + .($this->qb_offset ? $this->qb_offset.' TO '.($this->qb_limit + $this->qb_offset) : $this->qb_limit); + } + + return preg_replace('`SELECT`i', 'SELECT '.$select, $sql, 1); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + ibase_close($this->conn_id); + } + +} diff --git a/system/database/drivers/ibase/ibase_forge.php b/system/database/drivers/ibase/ibase_forge.php new file mode 100644 index 0000000..2c385f1 --- /dev/null +++ b/system/database/drivers/ibase/ibase_forge.php @@ -0,0 +1,252 @@ + 'INTEGER', + 'INTEGER' => 'INT64', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return bool + */ + public function create_database($db_name) + { + // Firebird databases are flat files, so a path is required + + // Hostname is needed for remote access + empty($this->db->hostname) OR $db_name = $this->hostname.':'.$db_name; + + return parent::create_database('"'.$db_name.'"'); + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + if ( ! ibase_drop_db($this->conn_id)) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identififers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET DEFAULT '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = 'UPDATE "RDB$RELATION_FIELDS" SET "RDB$NULL_FLAG" = ' + .($field[$i]['null'] === TRUE ? 'NULL' : '1') + .' WHERE "RDB$FIELD_NAME" = '.$this->db->escape($field[$i]['name']) + .' AND "RDB$RELATION_NAME" = '.$this->db->escape($table); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INT': + $attributes['TYPE'] = 'INTEGER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'INT64'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/ibase/ibase_result.php b/system/database/drivers/ibase/ibase_result.php new file mode 100644 index 0000000..900212e --- /dev/null +++ b/system/database/drivers/ibase/ibase_result.php @@ -0,0 +1,162 @@ +result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $num_fields = $this->num_fields(); $i < $num_fields; $i++) + { + $info = ibase_field_info($this->result_id, $i); + $field_names[] = $info['name']; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $info = ibase_field_info($this->result_id, $i); + + $retval[$i] = new stdClass(); + $retval[$i]->name = $info['name']; + $retval[$i]->type = $info['type']; + $retval[$i]->max_length = $info['length']; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + ibase_free_result($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return ibase_fetch_assoc($this->result_id, IBASE_FETCH_BLOBS); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = ibase_fetch_object($this->result_id, IBASE_FETCH_BLOBS); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} diff --git a/system/database/drivers/ibase/ibase_utility.php b/system/database/drivers/ibase/ibase_utility.php new file mode 100644 index 0000000..bc87508 --- /dev/null +++ b/system/database/drivers/ibase/ibase_utility.php @@ -0,0 +1,70 @@ +db->hostname, $this->db->username, $this->db->password)) + { + $res = ibase_backup($service, $this->db->database, $filename.'.fbk'); + + // Close the service connection + ibase_service_detach($service); + return $res; + } + + return FALSE; + } + +} diff --git a/system/database/drivers/ibase/index.html b/system/database/drivers/ibase/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/ibase/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/index.html b/system/database/drivers/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mssql/index.html b/system/database/drivers/mssql/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/mssql/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mssql/mssql_driver.php b/system/database/drivers/mssql/mssql_driver.php new file mode 100644 index 0000000..5012640 --- /dev/null +++ b/system/database/drivers/mssql/mssql_driver.php @@ -0,0 +1,519 @@ +port)) + { + $this->hostname .= (DIRECTORY_SEPARATOR === '\\' ? ',' : ':').$this->port; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $this->conn_id = ($persistent) + ? mssql_pconnect($this->hostname, $this->username, $this->password) + : mssql_connect($this->hostname, $this->username, $this->password); + + if ( ! $this->conn_id) + { + return FALSE; + } + + // ---------------------------------------------------------------- + + // Select the DB... assuming a database name is specified in the config file + if ($this->database !== '' && ! $this->db_select()) + { + log_message('error', 'Unable to select database: '.$this->database); + + return ($this->db_debug === TRUE) + ? $this->display_error('db_unable_to_select', $this->database) + : FALSE; + } + + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + // Note: Escaping is required in the event that the DB name + // contains reserved characters. + if (mssql_select_db('['.$database.']', $this->conn_id)) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return mixed resource if rows are returned, bool otherwise + */ + protected function _execute($sql) + { + return mssql_query($sql, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->simple_query('BEGIN TRAN'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->simple_query('COMMIT TRAN'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->simple_query('ROLLBACK TRAN'); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return mssql_rows_affected($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * Returns the last id created in the Identity column. + * + * @return string + */ + public function insert_id() + { + $query = version_compare($this->version(), '8', '>=') + ? 'SELECT SCOPE_IDENTITY() AS last_id' + : 'SELECT @@IDENTITY AS last_id'; + + $query = $this->query($query); + $query = $query->row(); + return $query->last_id; + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return (ini_set('mssql.charset', $charset) !== FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Version number query string + * + * @return string + */ + protected function _version() + { + return "SELECT SERVERPROPERTY('ProductVersion') AS ver"; + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + // We need this because the error info is discarded by the + // server the first time you request it, and query() already + // calls error() once for logging purposes when a query fails. + static $error = array('code' => 0, 'message' => NULL); + + $message = mssql_get_last_message(); + if ( ! empty($message)) + { + $error['code'] = $this->query('SELECT @@ERROR AS code')->row()->code; + $error['message'] = $message; + } + + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + $limit = $this->qb_offset + $this->qb_limit; + + // As of SQL Server 2005 (9.0.*) ROW_NUMBER() is supported, + // however an ORDER BY clause is required for it to work + if (version_compare($this->version(), '9', '>=') && $this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + mssql_close($this->conn_id); + } + +} diff --git a/system/database/drivers/mssql/mssql_forge.php b/system/database/drivers/mssql/mssql_forge.php new file mode 100644 index 0000000..f9dee91 --- /dev/null +++ b/system/database/drivers/mssql/mssql_forge.php @@ -0,0 +1,152 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/mssql/mssql_result.php b/system/database/drivers/mssql/mssql_result.php new file mode 100644 index 0000000..fbe2eb1 --- /dev/null +++ b/system/database/drivers/mssql/mssql_result.php @@ -0,0 +1,199 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = mssql_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return mssql_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + mssql_field_seek($this->result_id, 0); + while ($field = mssql_fetch_field($this->result_id)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field = mssql_fetch_field($this->result_id, $i); + + $retval[$i] = new stdClass(); + $retval[$i]->name = $field->name; + $retval[$i]->type = $field->type; + $retval[$i]->max_length = $field->max_length; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + mssql_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return mssql_data_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return mssql_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = mssql_fetch_object($this->result_id); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} diff --git a/system/database/drivers/mssql/mssql_utility.php b/system/database/drivers/mssql/mssql_utility.php new file mode 100644 index 0000000..a739dc8 --- /dev/null +++ b/system/database/drivers/mssql/mssql_utility.php @@ -0,0 +1,78 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/mysql/index.html b/system/database/drivers/mysql/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/mysql/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mysql/mysql_driver.php b/system/database/drivers/mysql/mysql_driver.php new file mode 100644 index 0000000..367f89a --- /dev/null +++ b/system/database/drivers/mysql/mysql_driver.php @@ -0,0 +1,495 @@ +port)) + { + $this->hostname .= ':'.$this->port; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $client_flags = ($this->compress === FALSE) ? 0 : MYSQL_CLIENT_COMPRESS; + + if ($this->encrypt === TRUE) + { + $client_flags = $client_flags | MYSQL_CLIENT_SSL; + } + + // Error suppression is necessary mostly due to PHP 5.5+ issuing E_DEPRECATED messages + $this->conn_id = ($persistent === TRUE) + ? mysql_pconnect($this->hostname, $this->username, $this->password, $client_flags) + : mysql_connect($this->hostname, $this->username, $this->password, TRUE, $client_flags); + + // ---------------------------------------------------------------- + + // Select the DB... assuming a database name is specified in the config file + if ($this->database !== '' && ! $this->db_select()) + { + log_message('error', 'Unable to select database: '.$this->database); + + return ($this->db_debug === TRUE) + ? $this->display_error('db_unable_to_select', $this->database) + : FALSE; + } + + if (isset($this->stricton) && is_resource($this->conn_id)) + { + if ($this->stricton) + { + $this->simple_query('SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'); + } + else + { + $this->simple_query( + 'SET SESSION sql_mode = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")' + ); + } + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if (mysql_ping($this->conn_id) === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if (mysql_select_db($database, $this->conn_id)) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return mysql_set_charset($charset, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($version = mysql_get_server_info($this->conn_id)) === FALSE) + { + return FALSE; + } + + return $this->data_cache['version'] = $version; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return mixed + */ + protected function _execute($sql) + { + return mysql_query($this->_prep_query($sql), $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Prep the query + * + * If needed, each database adapter can prep the query string + * + * @param string $sql an SQL query + * @return string + */ + protected function _prep_query($sql) + { + // mysql_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack + // modifies the query so that it a proper number of affected rows is returned. + if ($this->delete_hack === TRUE && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) + { + return trim($sql).' WHERE 1=1'; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->simple_query('SET AUTOCOMMIT=0'); + return $this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ($this->simple_query('COMMIT')) + { + $this->simple_query('SET AUTOCOMMIT=1'); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ($this->simple_query('ROLLBACK')) + { + $this->simple_query('SET AUTOCOMMIT=1'); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return mysql_real_escape_string($str, $this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return mysql_affected_rows($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return mysql_insert_id($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES FROM '.$this->_escape_char.$this->database.$this->_escape_char; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + return array('code' => mysql_errno($this->conn_id), 'message' => mysql_error($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + // Error suppression to avoid annoying E_WARNINGs in cases + // where the connection has already been closed for some reason. + @mysql_close($this->conn_id); + } + +} diff --git a/system/database/drivers/mysql/mysql_forge.php b/system/database/drivers/mysql/mysql_forge.php new file mode 100644 index 0000000..410ea2d --- /dev/null +++ b/system/database/drivers/mysql/mysql_forge.php @@ -0,0 +1,243 @@ +db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) + { + $sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set; + } + + if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE')) + { + $sql .= ' COLLATE = '.$this->db->dbcollat; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP') + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = ($alter_type === 'ADD') + ? "\n\tADD ".$field[$i]['_literal'] + : "\n\tMODIFY ".$field[$i]['_literal']; + } + else + { + if ($alter_type === 'ADD') + { + $field[$i]['_literal'] = "\n\tADD "; + } + else + { + $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + } + + $field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]); + } + } + + return array($sql.implode(',', $field)); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/mysql/mysql_result.php b/system/database/drivers/mysql/mysql_result.php new file mode 100644 index 0000000..05fc36e --- /dev/null +++ b/system/database/drivers/mysql/mysql_result.php @@ -0,0 +1,200 @@ +num_rows = mysql_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of rows in the result set + * + * @return int + */ + public function num_rows() + { + return $this->num_rows; + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return mysql_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + mysql_field_seek($this->result_id, 0); + while ($field = mysql_fetch_field($this->result_id)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = mysql_field_name($this->result_id, $i); + $retval[$i]->type = mysql_field_type($this->result_id, $i); + $retval[$i]->max_length = mysql_field_len($this->result_id, $i); + $retval[$i]->primary_key = (int) (strpos(mysql_field_flags($this->result_id, $i), 'primary_key') !== FALSE); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + mysql_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return $this->num_rows + ? mysql_data_seek($this->result_id, $n) + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return mysql_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return mysql_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/mysql/mysql_utility.php b/system/database/drivers/mysql/mysql_utility.php new file mode 100644 index 0000000..0564a5a --- /dev/null +++ b/system/database/drivers/mysql/mysql_utility.php @@ -0,0 +1,212 @@ +db->query('SHOW CREATE TABLE '.$this->db->escape_identifiers($this->db->database.'.'.$table)); + + // No result means the table name was invalid + if ($query === FALSE) + { + continue; + } + + // Write out the table schema + $output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline; + + if ($add_drop === TRUE) + { + $output .= 'DROP TABLE IF EXISTS '.$this->db->protect_identifiers($table).';'.$newline.$newline; + } + + $i = 0; + $result = $query->result_array(); + foreach ($result[0] as $val) + { + if ($i++ % 2) + { + $output .= $val.';'.$newline.$newline; + } + } + + // If inserts are not needed we're done... + if ($add_insert === FALSE) + { + continue; + } + + // Grab all the data from the current table + $query = $this->db->query('SELECT * FROM '.$this->db->protect_identifiers($table)); + + if ($query->num_rows() === 0) + { + continue; + } + + // Fetch the field names and determine if the field is an + // integer type. We use this info to decide whether to + // surround the data with quotes or not + + $i = 0; + $field_str = ''; + $is_int = array(); + while ($field = mysql_fetch_field($query->result_id)) + { + // Most versions of MySQL store timestamp as a string + $is_int[$i] = in_array(strtolower(mysql_field_type($query->result_id, $i)), + array('tinyint', 'smallint', 'mediumint', 'int', 'bigint'), //, 'timestamp'), + TRUE); + + // Create a string of field names + $field_str .= $this->db->escape_identifiers($field->name).', '; + $i++; + } + + // Trim off the end comma + $field_str = preg_replace('/, $/' , '', $field_str); + + // Build the insert string + foreach ($query->result_array() as $row) + { + $val_str = ''; + + $i = 0; + foreach ($row as $v) + { + // Is the value NULL? + if ($v === NULL) + { + $val_str .= 'NULL'; + } + else + { + // Escape the data if it's not an integer + $val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v; + } + + // Append a comma + $val_str .= ', '; + $i++; + } + + // Remove the comma at the end of the string + $val_str = preg_replace('/, $/' , '', $val_str); + + // Build the INSERT string + $output .= 'INSERT INTO '.$this->db->protect_identifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline; + } + + $output .= $newline.$newline; + } + + // Do we need to include a statement to re-enable foreign key checks? + if ($foreign_key_checks === FALSE) + { + $output .= 'SET foreign_key_checks = 1;'.$newline; + } + + return $output; + } + +} diff --git a/system/database/drivers/mysqli/index.html b/system/database/drivers/mysqli/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/mysqli/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/mysqli/mysqli_driver.php b/system/database/drivers/mysqli/mysqli_driver.php new file mode 100644 index 0000000..f5e9949 --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_driver.php @@ -0,0 +1,554 @@ +report_mode = MYSQLI_REPORT_OFF; + } + + // Do we have a socket path? + if ($this->hostname[0] === '/') + { + $hostname = NULL; + $port = NULL; + $socket = $this->hostname; + } + else + { + $hostname = ($persistent === TRUE) + ? 'p:'.$this->hostname : $this->hostname; + $port = empty($this->port) ? NULL : $this->port; + $socket = NULL; + } + + $client_flags = ($this->compress === TRUE) ? MYSQLI_CLIENT_COMPRESS : 0; + $this->_mysqli = mysqli_init(); + + $this->_mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); + + if (isset($this->stricton)) + { + if ($this->stricton) + { + $this->_mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'); + } + else + { + $this->_mysqli->options(MYSQLI_INIT_COMMAND, + 'SET SESSION sql_mode = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")' + ); + } + } + + if (is_array($this->encrypt)) + { + $ssl = array(); + empty($this->encrypt['ssl_key']) OR $ssl['key'] = $this->encrypt['ssl_key']; + empty($this->encrypt['ssl_cert']) OR $ssl['cert'] = $this->encrypt['ssl_cert']; + empty($this->encrypt['ssl_ca']) OR $ssl['ca'] = $this->encrypt['ssl_ca']; + empty($this->encrypt['ssl_capath']) OR $ssl['capath'] = $this->encrypt['ssl_capath']; + empty($this->encrypt['ssl_cipher']) OR $ssl['cipher'] = $this->encrypt['ssl_cipher']; + + if (isset($this->encrypt['ssl_verify'])) + { + $client_flags |= MYSQLI_CLIENT_SSL; + + if ($this->encrypt['ssl_verify']) + { + defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT') && $this->_mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, TRUE); + } + // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT + // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another + // constant ... + // + // https://secure.php.net/ChangeLog-5.php#5.6.16 + // https://bugs.php.net/bug.php?id=68344 + elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT')) + { + $client_flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + } + + if ( ! empty($ssl)) + { + $client_flags |= MYSQLI_CLIENT_SSL; + $this->_mysqli->ssl_set( + isset($ssl['key']) ? $ssl['key'] : NULL, + isset($ssl['cert']) ? $ssl['cert'] : NULL, + isset($ssl['ca']) ? $ssl['ca'] : NULL, + isset($ssl['capath']) ? $ssl['capath'] : NULL, + isset($ssl['cipher']) ? $ssl['cipher'] : NULL + ); + } + } + + if ($this->_mysqli->real_connect($hostname, $this->username, $this->password, $this->database, $port, $socket, $client_flags)) + { + // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails + if ( + ($client_flags & MYSQLI_CLIENT_SSL) + && version_compare($this->_mysqli->client_info, '5.7.3', '<=') + && empty($this->_mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value) + ) + { + $this->_mysqli->close(); + $message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!'; + log_message('error', $message); + return ($this->db_debug) ? $this->display_error($message, '', TRUE) : FALSE; + } + + return $this->_mysqli; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if ($this->conn_id !== FALSE && $this->conn_id->ping() === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if ($this->conn_id->select_db($database)) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return $this->conn_id->set_charset($charset); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + return $this->data_cache['version'] = $this->conn_id->server_info; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return mixed + */ + protected function _execute($sql) + { + return $this->conn_id->query($this->_prep_query($sql)); + } + + // -------------------------------------------------------------------- + + /** + * Prep the query + * + * If needed, each database adapter can prep the query string + * + * @param string $sql an SQL query + * @return string + */ + protected function _prep_query($sql) + { + // mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack + // modifies the query so that it a proper number of affected rows is returned. + if ($this->delete_hack === TRUE && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) + { + return trim($sql).' WHERE 1=1'; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->conn_id->autocommit(FALSE); + return is_php('5.5') + ? $this->conn_id->begin_transaction() + : $this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ($this->conn_id->commit()) + { + $this->conn_id->autocommit(TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ($this->conn_id->rollback()) + { + $this->conn_id->autocommit(TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return $this->conn_id->real_escape_string($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return $this->conn_id->affected_rows; + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return $this->conn_id->insert_id; + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES FROM '.$this->_escape_char.$this->database.$this->_escape_char; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + if ( ! empty($this->_mysqli->connect_errno)) + { + return array( + 'code' => $this->_mysqli->connect_errno, + 'message' => $this->_mysqli->connect_error + ); + } + + return array('code' => $this->conn_id->errno, 'message' => $this->conn_id->error); + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + $this->conn_id->close(); + } + +} diff --git a/system/database/drivers/mysqli/mysqli_forge.php b/system/database/drivers/mysqli/mysqli_forge.php new file mode 100644 index 0000000..992c772 --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_forge.php @@ -0,0 +1,245 @@ +db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) + { + $sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set; + } + + if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE')) + { + $sql .= ' COLLATE = '.$this->db->dbcollat; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP') + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = ($alter_type === 'ADD') + ? "\n\tADD ".$field[$i]['_literal'] + : "\n\tMODIFY ".$field[$i]['_literal']; + } + else + { + if ($alter_type === 'ADD') + { + $field[$i]['_literal'] = "\n\tADD "; + } + else + { + $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + } + + $field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]); + } + } + + return array($sql.implode(',', $field)); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/mysqli/mysqli_result.php b/system/database/drivers/mysqli/mysqli_result.php new file mode 100644 index 0000000..8c4f94d --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_result.php @@ -0,0 +1,233 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = $this->result_id->num_rows; + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return $this->result_id->field_count; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + $this->result_id->field_seek(0); + while ($field = $this->result_id->fetch_field()) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + $field_data = $this->result_id->fetch_fields(); + for ($i = 0, $c = count($field_data); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $field_data[$i]->name; + $retval[$i]->type = static::_get_field_type($field_data[$i]->type); + $retval[$i]->max_length = $field_data[$i]->max_length; + $retval[$i]->primary_key = (int) ($field_data[$i]->flags & MYSQLI_PRI_KEY_FLAG); + $retval[$i]->default = $field_data[$i]->def; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Get field type + * + * Extracts field type info from the bitflags returned by + * mysqli_result::fetch_fields() + * + * @used-by CI_DB_mysqli_result::field_data() + * @param int $type + * @return string + */ + private static function _get_field_type($type) + { + static $map; + isset($map) OR $map = array( + MYSQLI_TYPE_DECIMAL => 'decimal', + MYSQLI_TYPE_BIT => 'bit', + MYSQLI_TYPE_TINY => 'tinyint', + MYSQLI_TYPE_SHORT => 'smallint', + MYSQLI_TYPE_INT24 => 'mediumint', + MYSQLI_TYPE_LONG => 'int', + MYSQLI_TYPE_LONGLONG => 'bigint', + MYSQLI_TYPE_FLOAT => 'float', + MYSQLI_TYPE_DOUBLE => 'double', + MYSQLI_TYPE_TIMESTAMP => 'timestamp', + MYSQLI_TYPE_DATE => 'date', + MYSQLI_TYPE_TIME => 'time', + MYSQLI_TYPE_DATETIME => 'datetime', + MYSQLI_TYPE_YEAR => 'year', + MYSQLI_TYPE_NEWDATE => 'date', + MYSQLI_TYPE_INTERVAL => 'interval', + MYSQLI_TYPE_ENUM => 'enum', + MYSQLI_TYPE_SET => 'set', + MYSQLI_TYPE_TINY_BLOB => 'tinyblob', + MYSQLI_TYPE_MEDIUM_BLOB => 'mediumblob', + MYSQLI_TYPE_BLOB => 'blob', + MYSQLI_TYPE_LONG_BLOB => 'longblob', + MYSQLI_TYPE_STRING => 'char', + MYSQLI_TYPE_VAR_STRING => 'varchar', + MYSQLI_TYPE_GEOMETRY => 'geometry' + ); + + return isset($map[$type]) ? $map[$type] : $type; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_object($this->result_id)) + { + $this->result_id->free(); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return $this->result_id->data_seek($n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return $this->result_id->fetch_assoc(); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return $this->result_id->fetch_object($class_name); + } + +} diff --git a/system/database/drivers/mysqli/mysqli_utility.php b/system/database/drivers/mysqli/mysqli_utility.php new file mode 100644 index 0000000..6a7d419 --- /dev/null +++ b/system/database/drivers/mysqli/mysqli_utility.php @@ -0,0 +1,212 @@ +db->query('SHOW CREATE TABLE '.$this->db->escape_identifiers($this->db->database.'.'.$table)); + + // No result means the table name was invalid + if ($query === FALSE) + { + continue; + } + + // Write out the table schema + $output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline; + + if ($add_drop === TRUE) + { + $output .= 'DROP TABLE IF EXISTS '.$this->db->protect_identifiers($table).';'.$newline.$newline; + } + + $i = 0; + $result = $query->result_array(); + foreach ($result[0] as $val) + { + if ($i++ % 2) + { + $output .= $val.';'.$newline.$newline; + } + } + + // If inserts are not needed we're done... + if ($add_insert === FALSE) + { + continue; + } + + // Grab all the data from the current table + $query = $this->db->query('SELECT * FROM '.$this->db->protect_identifiers($table)); + + if ($query->num_rows() === 0) + { + continue; + } + + // Fetch the field names and determine if the field is an + // integer type. We use this info to decide whether to + // surround the data with quotes or not + + $i = 0; + $field_str = ''; + $is_int = array(); + while ($field = $query->result_id->fetch_field()) + { + // Most versions of MySQL store timestamp as a string + $is_int[$i] = in_array($field->type, array(MYSQLI_TYPE_TINY, MYSQLI_TYPE_SHORT, MYSQLI_TYPE_INT24, MYSQLI_TYPE_LONG), TRUE); + + // Create a string of field names + $field_str .= $this->db->escape_identifiers($field->name).', '; + $i++; + } + + // Trim off the end comma + $field_str = preg_replace('/, $/' , '', $field_str); + + // Build the insert string + foreach ($query->result_array() as $row) + { + $val_str = ''; + + $i = 0; + foreach ($row as $v) + { + // Is the value NULL? + if ($v === NULL) + { + $val_str .= 'NULL'; + } + else + { + // Escape the data if it's not an integer + $val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v; + } + + // Append a comma + $val_str .= ', '; + $i++; + } + + // Remove the comma at the end of the string + $val_str = preg_replace('/, $/' , '', $val_str); + + // Build the INSERT string + $output .= 'INSERT INTO '.$this->db->protect_identifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline; + } + + $output .= $newline.$newline; + } + + // Do we need to include a statement to re-enable foreign key checks? + if ($foreign_key_checks === FALSE) + { + $output .= 'SET foreign_key_checks = 1;'.$newline; + } + + return $output; + } + +} diff --git a/system/database/drivers/oci8/index.html b/system/database/drivers/oci8/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/oci8/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/oci8/oci8_driver.php b/system/database/drivers/oci8/oci8_driver.php new file mode 100644 index 0000000..7bb43b5 --- /dev/null +++ b/system/database/drivers/oci8/oci8_driver.php @@ -0,0 +1,712 @@ + '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS + // Easy Connect string (Oracle 10g+) + 'ec' => '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i', + 'in' => '/^[a-z0-9$_]+$/i' // Instance name (defined in tnsnames.ora) + ); + + /* Space characters don't have any effect when actually + * connecting, but can be a hassle while validating the DSN. + */ + $this->dsn = str_replace(array("\n", "\r", "\t", ' '), '', $this->dsn); + + if ($this->dsn !== '') + { + foreach ($valid_dsns as $regexp) + { + if (preg_match($regexp, $this->dsn)) + { + return; + } + } + } + + // Legacy support for TNS in the hostname configuration field + $this->hostname = str_replace(array("\n", "\r", "\t", ' '), '', $this->hostname); + if (preg_match($valid_dsns['tns'], $this->hostname)) + { + $this->dsn = $this->hostname; + return; + } + elseif ($this->hostname !== '' && strpos($this->hostname, '/') === FALSE && strpos($this->hostname, ':') === FALSE + && (( ! empty($this->port) && ctype_digit($this->port)) OR $this->database !== '')) + { + /* If the hostname field isn't empty, doesn't contain + * ':' and/or '/' and if port and/or database aren't + * empty, then the hostname field is most likely indeed + * just a hostname. Therefore we'll try and build an + * Easy Connect string from these 3 settings, assuming + * that the database field is a service name. + */ + $this->dsn = $this->hostname + .(( ! empty($this->port) && ctype_digit($this->port)) ? ':'.$this->port : '') + .($this->database !== '' ? '/'.ltrim($this->database, '/') : ''); + + if (preg_match($valid_dsns['ec'], $this->dsn)) + { + return; + } + } + + /* At this point, we can only try and validate the hostname and + * database fields separately as DSNs. + */ + if (preg_match($valid_dsns['ec'], $this->hostname) OR preg_match($valid_dsns['in'], $this->hostname)) + { + $this->dsn = $this->hostname; + return; + } + + $this->database = str_replace(array("\n", "\r", "\t", ' '), '', $this->database); + foreach ($valid_dsns as $regexp) + { + if (preg_match($regexp, $this->database)) + { + return; + } + } + + /* Well - OK, an empty string should work as well. + * PHP will try to use environment variables to + * determine which Oracle instance to connect to. + */ + $this->dsn = ''; + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + $func = ($persistent === TRUE) ? 'oci_pconnect' : 'oci_connect'; + return empty($this->char_set) + ? $func($this->username, $this->password, $this->dsn) + : $func($this->username, $this->password, $this->dsn, $this->char_set); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($version_string = oci_server_version($this->conn_id)) === FALSE) + { + return FALSE; + } + elseif (preg_match('#Release\s(\d+(?:\.\d+)+)#', $version_string, $match)) + { + return $this->data_cache['version'] = $match[1]; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + /* Oracle must parse the query before it is run. All of the actions with + * the query are based on the statement id returned by oci_parse(). + */ + if ($this->_reset_stmt_id === TRUE) + { + $this->stmt_id = oci_parse($this->conn_id, $sql); + } + + oci_set_prefetch($this->stmt_id, 1000); + return oci_execute($this->stmt_id, $this->commit_mode); + } + + // -------------------------------------------------------------------- + + /** + * Get cursor. Returns a cursor from the database + * + * @return resource + */ + public function get_cursor() + { + return $this->curs_id = oci_new_cursor($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Stored Procedure. Executes a stored procedure + * + * @param string package name in which the stored procedure is in + * @param string stored procedure name to execute + * @param array parameters + * @return mixed + * + * params array keys + * + * KEY OPTIONAL NOTES + * name no the name of the parameter should be in : format + * value no the value of the parameter. If this is an OUT or IN OUT parameter, + * this should be a reference to a variable + * type yes the type of the parameter + * length yes the max size of the parameter + */ + public function stored_procedure($package, $procedure, array $params) + { + if ($package === '' OR $procedure === '') + { + log_message('error', 'Invalid query: '.$package.'.'.$procedure); + return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE; + } + + // Build the query string + $sql = 'BEGIN '.$package.'.'.$procedure.'('; + + $have_cursor = FALSE; + foreach ($params as $param) + { + $sql .= $param['name'].','; + + if (isset($param['type']) && $param['type'] === OCI_B_CURSOR) + { + $have_cursor = TRUE; + } + } + $sql = trim($sql, ',').'); END;'; + + $this->_reset_stmt_id = FALSE; + $this->stmt_id = oci_parse($this->conn_id, $sql); + $this->_bind_params($params); + $result = $this->query($sql, FALSE, $have_cursor); + $this->_reset_stmt_id = TRUE; + return $result; + } + + // -------------------------------------------------------------------- + + /** + * Bind parameters + * + * @param array $params + * @return void + */ + protected function _bind_params($params) + { + if ( ! is_array($params) OR ! is_resource($this->stmt_id)) + { + return; + } + + foreach ($params as $param) + { + foreach (array('name', 'value', 'type', 'length') as $val) + { + if ( ! isset($param[$val])) + { + $param[$val] = ''; + } + } + + oci_bind_by_name($this->stmt_id, $param['name'], $param['value'], $param['length'], $param['type']); + } + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->commit_mode = OCI_NO_AUTO_COMMIT; + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + $this->commit_mode = OCI_COMMIT_ON_SUCCESS; + + return oci_commit($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + $this->commit_mode = OCI_COMMIT_ON_SUCCESS; + return oci_rollback($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return oci_num_rows($this->stmt_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + // not supported in oracle + return $this->display_error('db_unsupported_function'); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"'; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE + FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + + $length = ($query[$i]->CHAR_LENGTH > 0) + ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION; + if ($length === NULL) + { + $length = $query[$i]->DATA_LENGTH; + } + $retval[$i]->max_length = $length; + + $default = $query[$i]->DATA_DEFAULT; + if ($default === NULL && $query[$i]->NULLABLE === 'N') + { + $default = ''; + } + $retval[$i]->default = $default; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + // oci_error() returns an array that already contains + // 'code' and 'message' keys, but it can return false + // if there was no error .... + if (is_resource($this->curs_id)) + { + $error = oci_error($this->curs_id); + } + elseif (is_resource($this->stmt_id)) + { + $error = oci_error($this->stmt_id); + } + elseif (is_resource($this->conn_id)) + { + $error = oci_error($this->conn_id); + } + else + { + $error = oci_error(); + } + + return is_array($error) + ? $error + : array('code' => '', 'message' => ''); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _insert_batch($table, $keys, $values) + { + $keys = implode(', ', $keys); + $sql = "INSERT ALL\n"; + + for ($i = 0, $c = count($values); $i < $c; $i++) + { + $sql .= ' INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n"; + } + + return $sql.'SELECT * FROM dual'; + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + $this->where('rownum <= ',$this->qb_limit, FALSE); + $this->qb_limit = FALSE; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + if (version_compare($this->version(), '12.1', '>=')) + { + // OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + $this->limit_used = TRUE; + return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')' + .($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1) : ''); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + if (is_resource($this->curs_id)) + { + oci_free_statement($this->curs_id); + } + + if (is_resource($this->stmt_id)) + { + oci_free_statement($this->stmt_id); + } + + oci_close($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * We need to reset our $limit_used hack flag, so it doesn't propagate + * to subsequent queries. + * + * @return void + */ + protected function _reset_select() + { + $this->limit_used = FALSE; + parent::_reset_select(); + } +} diff --git a/system/database/drivers/oci8/oci8_forge.php b/system/database/drivers/oci8/oci8_forge.php new file mode 100644 index 0000000..9910b11 --- /dev/null +++ b/system/database/drivers/oci8/oci8_forge.php @@ -0,0 +1,217 @@ +db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = "\n\t".$field[$i]['_literal']; + } + else + { + $field[$i]['_literal'] = "\n\t".$this->_process_column($field[$i]); + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + + if ($alter_type === 'MODIFY' && ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + + $field[$i] = "\n\t".$field[$i]['_literal']; + } + } + + $sql .= ' '.$alter_type.' '; + $sql .= (count($field) === 1) + ? $field[0] + : '('.implode(',', $field).')'; + + // RENAME COLUMN must be executed after MODIFY + array_unshift($sqls, $sql); + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'number') !== FALSE && version_compare($this->db->version(), '12.1', '>=')) + { + $field['auto_increment'] = ' GENERATED ALWAYS AS IDENTITY'; + } + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['default'] + .$field['auto_increment'] + .$field['null'] + .$field['unique']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'INT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'NUMBER'; + return; + default: return; + } + } +} diff --git a/system/database/drivers/oci8/oci8_result.php b/system/database/drivers/oci8/oci8_result.php new file mode 100644 index 0000000..4312f9b --- /dev/null +++ b/system/database/drivers/oci8/oci8_result.php @@ -0,0 +1,230 @@ +stmt_id = $driver_object->stmt_id; + $this->curs_id = $driver_object->curs_id; + $this->limit_used = $driver_object->limit_used; + $this->commit_mode =& $driver_object->commit_mode; + $driver_object->stmt_id = FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + $count = oci_num_fields($this->stmt_id); + + // if we used a limit we subtract it + return ($this->limit_used) ? $count - 1 : $count; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($c = 1, $fieldCount = $this->num_fields(); $c <= $fieldCount; $c++) + { + $field_names[] = oci_field_name($this->stmt_id, $c); + } + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($c = 1, $fieldCount = $this->num_fields(); $c <= $fieldCount; $c++) + { + $F = new stdClass(); + $F->name = oci_field_name($this->stmt_id, $c); + $F->type = oci_field_type($this->stmt_id, $c); + $F->max_length = oci_field_size($this->stmt_id, $c); + + $retval[] = $F; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + oci_free_statement($this->result_id); + $this->result_id = FALSE; + } + + if (is_resource($this->stmt_id)) + { + oci_free_statement($this->stmt_id); + } + + if (is_resource($this->curs_id)) + { + oci_cancel($this->curs_id); + $this->curs_id = NULL; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + $id = ($this->curs_id) ? $this->curs_id : $this->stmt_id; + return oci_fetch_assoc($id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = ($this->curs_id) + ? oci_fetch_object($this->curs_id) + : oci_fetch_object($this->stmt_id); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} diff --git a/system/database/drivers/oci8/oci8_utility.php b/system/database/drivers/oci8/oci8_utility.php new file mode 100644 index 0000000..bcce114 --- /dev/null +++ b/system/database/drivers/oci8/oci8_utility.php @@ -0,0 +1,69 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/odbc/index.html b/system/database/drivers/odbc/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/odbc/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/odbc/odbc_driver.php b/system/database/drivers/odbc/odbc_driver.php new file mode 100644 index 0000000..cfb9d57 --- /dev/null +++ b/system/database/drivers/odbc/odbc_driver.php @@ -0,0 +1,426 @@ +dsn)) + { + $this->dsn = $this->hostname; + } + } + + // -------------------------------------------------------------------- + + /** + * Non-persistent database connection + * + * @param bool $persistent + * @return resource + */ + public function db_connect($persistent = FALSE) + { + return ($persistent === TRUE) + ? odbc_pconnect($this->dsn, $this->username, $this->password) + : odbc_connect($this->dsn, $this->username, $this->password); + } + + // -------------------------------------------------------------------- + + /** + * Compile Bindings + * + * @param string $sql SQL statement + * @param array $binds An array of values to bind + * @return string + */ + public function compile_binds($sql, $binds) + { + if (empty($binds) OR empty($this->bind_marker) OR strpos($sql, $this->bind_marker) === FALSE) + { + return $sql; + } + elseif ( ! is_array($binds)) + { + $binds = array($binds); + $bind_count = 1; + } + else + { + // Make sure we're using numeric keys + $binds = array_values($binds); + $bind_count = count($binds); + } + + // We'll need the marker length later + $ml = strlen($this->bind_marker); + + // Make sure not to replace a chunk inside a string that happens to match the bind marker + if ($c = preg_match_all("/'[^']*'|\"[^\"]*\"/i", $sql, $matches)) + { + $c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', + str_replace($matches[0], + str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]), + $sql, $c), + $matches, PREG_OFFSET_CAPTURE); + + // Bind values' count must match the count of markers in the query + if ($bind_count !== $c) + { + return $sql; + } + } + elseif (($c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count) + { + return $sql; + } + + if ($this->bind_marker !== '?') + { + do + { + $c--; + $sql = substr_replace($sql, '?', $matches[0][$c][1], $ml); + } + while ($c !== 0); + } + + if (FALSE !== ($this->odbc_result = odbc_prepare($this->conn_id, $sql))) + { + $this->binds = array_values($binds); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + if ( ! isset($this->odbc_result)) + { + return odbc_exec($this->conn_id, $sql); + } + elseif ($this->odbc_result === FALSE) + { + return FALSE; + } + + if (TRUE === ($success = odbc_execute($this->odbc_result, $this->binds))) + { + // For queries that return result sets, return the result_id resource on success + $this->is_write_type($sql) OR $success = $this->odbc_result; + } + + $this->odbc_result = NULL; + $this->binds = array(); + + return $success; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return odbc_autocommit($this->conn_id, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if (odbc_commit($this->conn_id)) + { + odbc_autocommit($this->conn_id, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if (odbc_rollback($this->conn_id)) + { + odbc_autocommit($this->conn_id, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + $this->display_error('db_unsupported_feature'); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return odbc_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return bool + */ + public function insert_id() + { + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '".$this->schema."'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." AND table_name LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Field data query + * + * Generates a platform-specific query so that the column data can be retrieved + * + * @param string $table + * @return string + */ + protected function _field_data($table) + { + return 'SELECT TOP 1 FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + return array('code' => odbc_error($this->conn_id), 'message' => odbc_errormsg($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + odbc_close($this->conn_id); + } +} diff --git a/system/database/drivers/odbc/odbc_forge.php b/system/database/drivers/odbc/odbc_forge.php new file mode 100644 index 0000000..115d08a --- /dev/null +++ b/system/database/drivers/odbc/odbc_forge.php @@ -0,0 +1,87 @@ +num_rows)) + { + return $this->num_rows; + } + elseif (($this->num_rows = odbc_num_rows($this->result_id)) !== -1) + { + return $this->num_rows; + } + + // Work-around for ODBC subdrivers that don't support num_rows() + if (count($this->result_array) > 0) + { + return $this->num_rows = count($this->result_array); + } + elseif (count($this->result_object) > 0) + { + return $this->num_rows = count($this->result_object); + } + + return $this->num_rows = count($this->result_array()); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return odbc_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + $num_fields = $this->num_fields(); + + if ($num_fields > 0) + { + for ($i = 1; $i <= $num_fields; $i++) + { + $field_names[] = odbc_field_name($this->result_id, $i); + } + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $odbc_index = 1, $c = $this->num_fields(); $i < $c; $i++, $odbc_index++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = odbc_field_name($this->result_id, $odbc_index); + $retval[$i]->type = odbc_field_type($this->result_id, $odbc_index); + $retval[$i]->max_length = odbc_field_len($this->result_id, $odbc_index); + $retval[$i]->primary_key = 0; + $retval[$i]->default = ''; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + odbc_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return odbc_fetch_array($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + $row = odbc_fetch_object($this->result_id); + + if ($class_name === 'stdClass' OR ! $row) + { + return $row; + } + + $class_name = new $class_name(); + foreach ($row as $key => $value) + { + $class_name->$key = $value; + } + + return $class_name; + } + +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('odbc_fetch_array')) +{ + /** + * ODBC Fetch array + * + * Emulates the native odbc_fetch_array() function when + * it is not available (odbc_fetch_array() requires unixODBC) + * + * @param resource &$result + * @param int $rownumber + * @return array + */ + function odbc_fetch_array(&$result, $rownumber = 1) + { + $rs = array(); + if ( ! odbc_fetch_into($result, $rs, $rownumber)) + { + return FALSE; + } + + $rs_assoc = array(); + foreach ($rs as $k => $v) + { + $field_name = odbc_field_name($result, $k+1); + $rs_assoc[$field_name] = $v; + } + + return $rs_assoc; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('odbc_fetch_object')) +{ + /** + * ODBC Fetch object + * + * Emulates the native odbc_fetch_object() function when + * it is not available. + * + * @param resource &$result + * @param int $rownumber + * @return object + */ + function odbc_fetch_object(&$result, $rownumber = 1) + { + $rs = array(); + if ( ! odbc_fetch_into($result, $rs, $rownumber)) + { + return FALSE; + } + + $rs_object = new stdClass(); + foreach ($rs as $k => $v) + { + $field_name = odbc_field_name($result, $k+1); + $rs_object->$field_name = $v; + } + + return $rs_object; + } +} diff --git a/system/database/drivers/odbc/odbc_utility.php b/system/database/drivers/odbc/odbc_utility.php new file mode 100644 index 0000000..a69ed00 --- /dev/null +++ b/system/database/drivers/odbc/odbc_utility.php @@ -0,0 +1,64 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/pdo/index.html b/system/database/drivers/pdo/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/pdo/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/pdo/pdo_driver.php b/system/database/drivers/pdo/pdo_driver.php new file mode 100644 index 0000000..559e865 --- /dev/null +++ b/system/database/drivers/pdo/pdo_driver.php @@ -0,0 +1,351 @@ +dsn, $match) && count($match) === 2) + { + // If there is a minimum valid dsn string pattern found, we're done + // This is for general PDO users, who tend to have a full DSN string. + $this->subdriver = $match[1]; + return; + } + // Legacy support for DSN specified in the hostname field + elseif (preg_match('/([^:]+):/', $this->hostname, $match) && count($match) === 2) + { + $this->dsn = $this->hostname; + $this->hostname = NULL; + $this->subdriver = $match[1]; + return; + } + elseif (in_array($this->subdriver, array('mssql', 'sybase'), TRUE)) + { + $this->subdriver = 'dblib'; + } + elseif ($this->subdriver === '4D') + { + $this->subdriver = '4d'; + } + elseif ( ! in_array($this->subdriver, array('4d', 'cubrid', 'dblib', 'firebird', 'ibm', 'informix', 'mysql', 'oci', 'odbc', 'pgsql', 'sqlite', 'sqlsrv'), TRUE)) + { + log_message('error', 'PDO: Invalid or non-existent subdriver'); + + if ($this->db_debug) + { + show_error('Invalid or non-existent PDO subdriver'); + } + } + + $this->dsn = NULL; + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if ($persistent === TRUE) + { + $this->options[PDO::ATTR_PERSISTENT] = TRUE; + } + + // From PHP8.0, default PDO::ATTR_ERRMODE is changed + // from PDO::ERRMODE_SILENT to PDO::ERRMODE_EXCEPTION + // as https://wiki.php.net/rfc/pdo_default_errmode + if ( ! isset($this->options[PDO::ATTR_ERRMODE])) + { + $this->options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_SILENT; + } + + try + { + return new PDO($this->dsn, $this->username, $this->password, $this->options); + } + catch (PDOException $e) + { + if ($this->db_debug && empty($this->failover)) + { + $this->display_error($e->getMessage(), '', TRUE); + } + + return FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + // Not all subdrivers support the getAttribute() method + try + { + return $this->data_cache['version'] = $this->conn_id->getAttribute(PDO::ATTR_SERVER_VERSION); + } + catch (PDOException $e) + { + return parent::version(); + } + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql SQL query + * @return mixed + */ + protected function _execute($sql) + { + return $this->conn_id->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->conn_id->beginTransaction(); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->conn_id->commit(); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->conn_id->rollBack(); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + // Escape the string + $str = $this->conn_id->quote($str); + + // If there are duplicated quotes, trim them away + return ($str[0] === "'") + ? substr($str, 1, -1) + : $str; + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return is_object($this->result_id) ? $this->result_id->rowCount() : 0; + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @param string $name + * @return int + */ + public function insert_id($name = NULL) + { + return $this->conn_id->lastInsertId($name); + } + + // -------------------------------------------------------------------- + + /** + * Field data query + * + * Generates a platform-specific query so that the column data can be retrieved + * + * @param string $table + * @return string + */ + protected function _field_data($table) + { + return 'SELECT TOP 1 * FROM '.$this->protect_identifiers($table); + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + $error = array('code' => '00000', 'message' => ''); + $pdo_error = $this->conn_id->errorInfo(); + + if (empty($pdo_error[0])) + { + return $error; + } + + $error['code'] = isset($pdo_error[1]) ? $pdo_error[0].'/'.$pdo_error[1] : $pdo_error[0]; + if (isset($pdo_error[2])) + { + $error['message'] = $pdo_error[2]; + } + + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + $this->result_id = FALSE; + $this->conn_id = FALSE; + } + +} diff --git a/system/database/drivers/pdo/pdo_forge.php b/system/database/drivers/pdo/pdo_forge.php new file mode 100644 index 0000000..b35ff67 --- /dev/null +++ b/system/database/drivers/pdo/pdo_forge.php @@ -0,0 +1,66 @@ +num_rows)) + { + return $this->num_rows; + } + elseif (count($this->result_array) > 0) + { + return $this->num_rows = count($this->result_array); + } + elseif (count($this->result_object) > 0) + { + return $this->num_rows = count($this->result_object); + } + elseif (($num_rows = $this->result_id->rowCount()) > 0) + { + return $this->num_rows = $num_rows; + } + + return $this->num_rows = count($this->result_array()); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return $this->result_id->columnCount(); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return bool + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + // Might trigger an E_WARNING due to not all subdrivers + // supporting getColumnMeta() + $field_names[$i] = @$this->result_id->getColumnMeta($i); + $field_names[$i] = $field_names[$i]['name']; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + try + { + $retval = array(); + + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field = $this->result_id->getColumnMeta($i); + + $retval[$i] = new stdClass(); + $retval[$i]->name = $field['name']; + $retval[$i]->type = isset($field['native_type']) ? $field['native_type'] : null; + $retval[$i]->max_length = ($field['len'] > 0) ? $field['len'] : NULL; + $retval[$i]->primary_key = (int) ( ! empty($field['flags']) && in_array('primary_key', $field['flags'], TRUE)); + } + + return $retval; + } + catch (Exception $e) + { + if ($this->db->db_debug) + { + return $this->db->display_error('db_unsupported_feature'); + } + + return FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_object($this->result_id)) + { + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return $this->result_id->fetch(PDO::FETCH_ASSOC); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return $this->result_id->fetchObject($class_name); + } + +} diff --git a/system/database/drivers/pdo/pdo_utility.php b/system/database/drivers/pdo/pdo_utility.php new file mode 100644 index 0000000..2094ef4 --- /dev/null +++ b/system/database/drivers/pdo/pdo_utility.php @@ -0,0 +1,64 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/index.html b/system/database/drivers/pdo/subdrivers/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php b/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php new file mode 100644 index 0000000..8d5b2f6 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php @@ -0,0 +1,201 @@ +dsn)) + { + $this->dsn = '4D:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 3) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('TABLE_NAME').' FROM '.$this->escape_identifiers('_USER_TABLES'); + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' WHERE '.$this->escape_identifiers('TABLE_NAME')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT '.$this->escape_identifiers('COLUMN_NAME').' FROM '.$this->escape_identifiers('_USER_COLUMNS') + .' WHERE '.$this->escape_identifiers('TABLE_NAME').' = '.$this->escape($table); + } + + // -------------------------------------------------------------------- + + /** + * Field data query + * + * Generates a platform-specific query so that the column data can be retrieved + * + * @param string $table + * @return string + */ + protected function _field_data($table) + { + return 'SELECT * FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE).' LIMIT 1'; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : ''); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php b/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php new file mode 100644 index 0000000..28fc008 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php @@ -0,0 +1,218 @@ + 'INT', + 'SMALLINT' => 'INT', + 'INT' => 'INT64', + 'INT32' => 'INT64' + ); + + /** + * DEFAULT value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_default = FALSE; + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + // No method of modifying columns is supported + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['null'] + .$field['unique'] + .$field['auto_increment']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'INT64'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE'; + + // UNIQUE must be used with NOT NULL + $field['null'] = ' NOT NULL'; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE) + { + if (stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' AUTO_INCREMENT'; + } + elseif (strcasecmp($field['type'], 'UUID') === 0) + { + $field['auto_increment'] = ' AUTO_GENERATE'; + } + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php b/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php new file mode 100644 index 0000000..c8f9258 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php @@ -0,0 +1,210 @@ +dsn)) + { + $this->dsn = 'cubrid:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES'; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php b/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php new file mode 100644 index 0000000..de02983 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php @@ -0,0 +1,231 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'BIGINT' => 'NUMERIC', + 'FLOAT' => 'DOUBLE', + 'REAL' => 'DOUBLE' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $sqls[] = $sql.' CHANGE '.$field[$i]['_literal']; + } + else + { + $alter_type = empty($field[$i]['new_name']) ? ' MODIFY ' : ' CHANGE '; + $sqls[] = $sql.$alter_type.$this->_process_column($field[$i]); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'LONGTEXT': + $attributes['TYPE'] = 'STRING'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php b/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php new file mode 100644 index 0000000..7d8d4a2 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php @@ -0,0 +1,354 @@ +dsn)) + { + $this->dsn = $params['subdriver'].':host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + if ( ! empty($this->port)) + { + $this->dsn .= (DIRECTORY_SEPARATOR === '\\' ? ',' : ':').$this->port; + } + + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + empty($this->appname) OR $this->dsn .= ';appname='.$this->appname; + } + else + { + if ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 6) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + + $this->subdriver = 'dblib'; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if ($persistent === TRUE) + { + log_message('debug', "dblib driver doesn't support persistent connections"); + } + + $this->conn_id = parent::db_connect(FALSE); + + if ( ! is_object($this->conn_id)) + { + return $this->conn_id; + } + + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + $limit = $this->qb_offset + $this->qb_limit; + + // As of SQL Server 2005 (9.0.*) ROW_NUMBER() is supported, + // however an ORDER BY clause is required for it to work + if (version_compare($this->version(), '9', '>=') && $this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + return $this->data_cache['version'] = $this->conn_id->query("SELECT SERVERPROPERTY('ProductVersion') AS ver")->fetchColumn(0); + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php b/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php new file mode 100644 index 0000000..3ee352f --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php @@ -0,0 +1,150 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php b/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php new file mode 100644 index 0000000..9778250 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php @@ -0,0 +1,280 @@ +dsn)) + { + $this->dsn = 'firebird:'; + + if ( ! empty($this->database)) + { + $this->dsn .= 'dbname='.$this->database; + } + elseif ( ! empty($this->hostname)) + { + $this->dsn .= 'dbname='.$this->hostname; + } + + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + empty($this->role) OR $this->dsn .= ';role='.$this->role; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 9) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "RDB$RELATION_NAME" FROM "RDB$RELATIONS" WHERE "RDB$RELATION_NAME" NOT LIKE \'RDB$%\' AND "RDB$RELATION_NAME" NOT LIKE \'MON$%\''; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' AND "RDB$RELATION_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT "RDB$FIELD_NAME" FROM "RDB$RELATION_FIELDS" WHERE "RDB$RELATION_NAME" = '.$this->escape($table); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "rfields"."RDB$FIELD_NAME" AS "name", + CASE "fields"."RDB$FIELD_TYPE" + WHEN 7 THEN \'SMALLINT\' + WHEN 8 THEN \'INTEGER\' + WHEN 9 THEN \'QUAD\' + WHEN 10 THEN \'FLOAT\' + WHEN 11 THEN \'DFLOAT\' + WHEN 12 THEN \'DATE\' + WHEN 13 THEN \'TIME\' + WHEN 14 THEN \'CHAR\' + WHEN 16 THEN \'INT64\' + WHEN 27 THEN \'DOUBLE\' + WHEN 35 THEN \'TIMESTAMP\' + WHEN 37 THEN \'VARCHAR\' + WHEN 40 THEN \'CSTRING\' + WHEN 261 THEN \'BLOB\' + ELSE NULL + END AS "type", + "fields"."RDB$FIELD_LENGTH" AS "max_length", + "rfields"."RDB$DEFAULT_VALUE" AS "default" + FROM "RDB$RELATION_FIELDS" "rfields" + JOIN "RDB$FIELDS" "fields" ON "rfields"."RDB$FIELD_SOURCE" = "fields"."RDB$FIELD_NAME" + WHERE "rfields"."RDB$RELATION_NAME" = '.$this->escape($table).' + ORDER BY "rfields"."RDB$FIELD_POSITION"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // Limit clause depends on if Interbase or Firebird + if (stripos($this->version(), 'firebird') !== FALSE) + { + $select = 'FIRST '.$this->qb_limit + .($this->qb_offset > 0 ? ' SKIP '.$this->qb_offset : ''); + } + else + { + $select = 'ROWS ' + .($this->qb_offset > 0 ? $this->qb_offset.' TO '.($this->qb_limit + $this->qb_offset) : $this->qb_limit); + } + + return preg_replace('`SELECT`i', 'SELECT '.$select, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php b/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php new file mode 100644 index 0000000..26e052a --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php @@ -0,0 +1,238 @@ + 'INTEGER', + 'INTEGER' => 'INT64', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return string + */ + public function create_database($db_name) + { + // Firebird databases are flat files, so a path is required + + // Hostname is needed for remote access + empty($this->db->hostname) OR $db_name = $this->hostname.':'.$db_name; + + return parent::create_database('"'.$db_name.'"'); + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + if ( ! ibase_drop_db($this->conn_id)) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = 'UPDATE "RDB$RELATION_FIELDS" SET "RDB$NULL_FLAG" = ' + .($field[$i]['null'] === TRUE ? 'NULL' : '1') + .' WHERE "RDB$FIELD_NAME" = '.$this->db->escape($field[$i]['name']) + .' AND "RDB$RELATION_NAME" = '.$this->db->escape($table); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INT': + $attributes['TYPE'] = 'INTEGER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'INT64'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php b/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php new file mode 100644 index 0000000..aca58ec --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php @@ -0,0 +1,245 @@ +dsn)) + { + $this->dsn = 'ibm:'; + + // Pre-defined DSN + if (empty($this->hostname) && empty($this->HOSTNAME) && empty($this->port) && empty($this->PORT)) + { + if (isset($this->DSN)) + { + $this->dsn .= 'DSN='.$this->DSN; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DSN='.$this->database; + } + + return; + } + + $this->dsn .= 'DRIVER='.(isset($this->DRIVER) ? '{'.$this->DRIVER.'}' : '{IBM DB2 ODBC DRIVER}').';'; + + if (isset($this->DATABASE)) + { + $this->dsn .= 'DATABASE='.$this->DATABASE.';'; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DATABASE='.$this->database.';'; + } + + if (isset($this->HOSTNAME)) + { + $this->dsn .= 'HOSTNAME='.$this->HOSTNAME.';'; + } + else + { + $this->dsn .= 'HOSTNAME='.(empty($this->hostname) ? '127.0.0.1;' : $this->hostname.';'); + } + + if (isset($this->PORT)) + { + $this->dsn .= 'PORT='.$this->port.';'; + } + elseif ( ! empty($this->port)) + { + $this->dsn .= ';PORT='.$this->port.';'; + } + + $this->dsn .= 'PROTOCOL='.(isset($this->PROTOCOL) ? $this->PROTOCOL.';' : 'TCPIP;'); + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "tabname" FROM "syscat"."tables" + WHERE "type" = \'T\' AND LOWER("tabschema") = '.$this->escape(strtolower($this->database)); + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND "tabname" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return array + */ + protected function _list_columns($table = '') + { + return 'SELECT "colname" FROM "syscat"."columns" + WHERE LOWER("tabschema") = '.$this->escape(strtolower($this->database)).' + AND LOWER("tabname") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "colname" AS "name", "typename" AS "type", "default" AS "default", "length" AS "max_length", + CASE "keyseq" WHEN NULL THEN 0 ELSE 1 END AS "primary_key" + FROM "syscat"."columns" + WHERE LOWER("tabschema") = '.$this->escape(strtolower($this->database)).' + AND LOWER("tabname") = '.$this->escape(strtolower($table)).' + ORDER BY "colno"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + $sql .= ' FETCH FIRST '.($this->qb_limit + $this->qb_offset).' ROWS ONLY'; + + return ($this->qb_offset) + ? 'SELECT * FROM ('.$sql.') WHERE rownum > '.$this->qb_offset + : $sql; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php b/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php new file mode 100644 index 0000000..cf023d4 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php @@ -0,0 +1,155 @@ + 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT' + ); + + /** + * DEFAULT value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_default = FALSE; + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'CHANGE') + { + $alter_type = 'MODIFY'; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE'; + + // UNIQUE must be used with NOT NULL + $field['null'] = ' NOT NULL'; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php b/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php new file mode 100644 index 0000000..4d230c3 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php @@ -0,0 +1,310 @@ +dsn)) + { + $this->dsn = 'informix:'; + + // Pre-defined DSN + if (empty($this->hostname) && empty($this->host) && empty($this->port) && empty($this->service)) + { + if (isset($this->DSN)) + { + $this->dsn .= 'DSN='.$this->DSN; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DSN='.$this->database; + } + + return; + } + + if (isset($this->host)) + { + $this->dsn .= 'host='.$this->host; + } + else + { + $this->dsn .= 'host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + } + + if (isset($this->service)) + { + $this->dsn .= '; service='.$this->service; + } + elseif ( ! empty($this->port)) + { + $this->dsn .= '; service='.$this->port; + } + + empty($this->database) OR $this->dsn .= '; database='.$this->database; + empty($this->server) OR $this->dsn .= '; server='.$this->server; + + $this->dsn .= '; protocol='.(isset($this->protocol) ? $this->protocol : 'onsoctcp') + .'; EnableScrollableCursors=1'; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "tabname" FROM "systables" + WHERE "tabid" > 99 AND "tabtype" = \'T\' AND LOWER("owner") = '.$this->escape(strtolower($this->username)); + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND "tabname" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + return 'SELECT "colname" FROM "systables", "syscolumns" + WHERE "systables"."tabid" = "syscolumns"."tabid" + AND "systables"."tabtype" = \'T\' + AND LOWER("systables"."owner") = '.$this->escape(strtolower($owner)).' + AND LOWER("systables"."tabname") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "syscolumns"."colname" AS "name", + CASE "syscolumns"."coltype" + WHEN 0 THEN \'CHAR\' + WHEN 1 THEN \'SMALLINT\' + WHEN 2 THEN \'INTEGER\' + WHEN 3 THEN \'FLOAT\' + WHEN 4 THEN \'SMALLFLOAT\' + WHEN 5 THEN \'DECIMAL\' + WHEN 6 THEN \'SERIAL\' + WHEN 7 THEN \'DATE\' + WHEN 8 THEN \'MONEY\' + WHEN 9 THEN \'NULL\' + WHEN 10 THEN \'DATETIME\' + WHEN 11 THEN \'BYTE\' + WHEN 12 THEN \'TEXT\' + WHEN 13 THEN \'VARCHAR\' + WHEN 14 THEN \'INTERVAL\' + WHEN 15 THEN \'NCHAR\' + WHEN 16 THEN \'NVARCHAR\' + WHEN 17 THEN \'INT8\' + WHEN 18 THEN \'SERIAL8\' + WHEN 19 THEN \'SET\' + WHEN 20 THEN \'MULTISET\' + WHEN 21 THEN \'LIST\' + WHEN 22 THEN \'Unnamed ROW\' + WHEN 40 THEN \'LVARCHAR\' + WHEN 41 THEN \'BLOB/CLOB/BOOLEAN\' + WHEN 4118 THEN \'Named ROW\' + ELSE "syscolumns"."coltype" + END AS "type", + "syscolumns"."collength" as "max_length", + CASE "sysdefaults"."type" + WHEN \'L\' THEN "sysdefaults"."default" + ELSE NULL + END AS "default" + FROM "syscolumns", "systables", "sysdefaults" + WHERE "syscolumns"."tabid" = "systables"."tabid" + AND "systables"."tabid" = "sysdefaults"."tabid" + AND "syscolumns"."colno" = "sysdefaults"."colno" + AND "systables"."tabtype" = \'T\' + AND LOWER("systables"."owner") = '.$this->escape(strtolower($this->username)).' + AND LOWER("systables"."tabname") = '.$this->escape(strtolower($table)).' + ORDER BY "syscolumns"."colno"'; + + return (($query = $this->query($sql)) !== FALSE) + ? $query->result_object() + : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE ONLY '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql $SQL Query + * @return string + */ + protected function _limit($sql) + { + $select = 'SELECT '.($this->qb_offset ? 'SKIP '.$this->qb_offset : '').'FIRST '.$this->qb_limit.' '; + return preg_replace('/^(SELECT\s)/i', $select, $sql, 1); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php b/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php new file mode 100644 index 0000000..368d8dc --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php @@ -0,0 +1,164 @@ + 'INTEGER', + 'INT' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'REAL' => 'DOUBLE PRECISION', + 'SMALLFLOAT' => 'DOUBLE PRECISION' + ); + + /** + * DEFAULT value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_default = ', '; + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'CHANGE') + { + $alter_type = 'MODIFY'; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'BYTE': + case 'TEXT': + case 'BLOB': + case 'CLOB': + $attributes['UNIQUE'] = FALSE; + if (isset($attributes['DEFAULT'])) + { + unset($attributes['DEFAULT']); + } + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute UNIQUE + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_unique(&$attributes, &$field) + { + if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE) + { + $field['unique'] = ' UNIQUE CONSTRAINT '.$this->db->escape_identifiers($field['name']); + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + // Not supported + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php new file mode 100644 index 0000000..1ad854d --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php @@ -0,0 +1,380 @@ +dsn)) + { + $this->dsn = 'mysql:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 6) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if (isset($this->stricton)) + { + if ($this->stricton) + { + $sql = 'CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'; + } + else + { + $sql = 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")'; + } + + if ( ! empty($sql)) + { + if (empty($this->options[PDO::MYSQL_ATTR_INIT_COMMAND])) + { + $this->options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode = '.$sql; + } + else + { + $this->options[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', @@session.sql_mode = '.$sql; + } + } + } + + if ($this->compress === TRUE) + { + $this->options[PDO::MYSQL_ATTR_COMPRESS] = TRUE; + } + + if (is_array($this->encrypt)) + { + $ssl = array(); + empty($this->encrypt['ssl_key']) OR $ssl[PDO::MYSQL_ATTR_SSL_KEY] = $this->encrypt['ssl_key']; + empty($this->encrypt['ssl_cert']) OR $ssl[PDO::MYSQL_ATTR_SSL_CERT] = $this->encrypt['ssl_cert']; + empty($this->encrypt['ssl_ca']) OR $ssl[PDO::MYSQL_ATTR_SSL_CA] = $this->encrypt['ssl_ca']; + empty($this->encrypt['ssl_capath']) OR $ssl[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->encrypt['ssl_capath']; + empty($this->encrypt['ssl_cipher']) OR $ssl[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->encrypt['ssl_cipher']; + + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') && isset($this->encrypt['ssl_verify'])) + { + $ssl[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->encrypt['ssl_verify']; + } + + // DO NOT use array_merge() here! + // It re-indexes numeric keys and the PDO_MYSQL_ATTR_SSL_* constants are integers. + empty($ssl) OR $this->options += $ssl; + } + + // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails + if ( + ($pdo = parent::db_connect($persistent)) !== FALSE + && ! empty($ssl) + && version_compare($pdo->getAttribute(PDO::ATTR_CLIENT_VERSION), '5.7.3', '<=') + && empty($pdo->query("SHOW STATUS LIKE 'ssl_cipher'")->fetchObject()->Value) + ) + { + $message = 'PDO_MYSQL was configured for an SSL connection, but got an unencrypted connection instead!'; + log_message('error', $message); + return ($this->db_debug) ? $this->display_error($message, '', TRUE) : FALSE; + } + + return $pdo; + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if (FALSE !== $this->simple_query('USE '.$this->escape_identifiers($database))) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + $this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE); + return $this->conn_id->beginTransaction(); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + if ($this->conn_id->commit()) + { + $this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + if ($this->conn_id->rollBack()) + { + $this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SHOW TABLES FROM '.$this->_escape_char.$this->database.$this->_escape_char; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'"; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->Field; + + sscanf($query[$i]->Type, '%[a-z](%d)', + $retval[$i]->type, + $retval[$i]->max_length + ); + + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * FROM tables + * + * Groups tables in FROM clauses if needed, so there is no confusion + * about operator precedence. + * + * @return string + */ + protected function _from_tables() + { + if ( ! empty($this->qb_join) && count($this->qb_from) > 1) + { + return '('.implode(', ', $this->qb_from).')'; + } + + return implode(', ', $this->qb_from); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php new file mode 100644 index 0000000..8bf5cfb --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php @@ -0,0 +1,257 @@ +db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) + { + $sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set; + } + + if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE')) + { + $sql .= ' COLLATE = '.$this->db->dbcollat; + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP') + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = ($alter_type === 'ADD') + ? "\n\tADD ".$field[$i]['_literal'] + : "\n\tMODIFY ".$field[$i]['_literal']; + } + else + { + if ($alter_type === 'ADD') + { + $field[$i]['_literal'] = "\n\tADD "; + } + else + { + $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + } + + $field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]); + } + } + + return array($sql.implode(',', $field)); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + $extra_clause = isset($field['after']) + ? ' AFTER '.$this->db->escape_identifiers($field['after']) : ''; + + if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) + { + $extra_clause = ' FIRST'; + } + + return $this->db->escape_identifiers($field['name']) + .(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name'])) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['null'] + .$field['default'] + .$field['auto_increment'] + .$field['unique'] + .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) + .$extra_clause; + } + + // -------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table (ignored) + * @return string + */ + protected function _process_indexes($table) + { + $sql = ''; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + if (is_array($this->keys[$i])) + { + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if ( ! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + continue; + } + } + } + elseif ( ! isset($this->fields[$this->keys[$i]])) + { + unset($this->keys[$i]); + continue; + } + + is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + + $sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i])) + .' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')'; + } + + $this->keys = array(); + + return $sql; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php b/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php new file mode 100644 index 0000000..3573691 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php @@ -0,0 +1,327 @@ +dsn)) + { + $this->dsn = 'oci:dbname='; + + // Oracle has a slightly different PDO DSN format (Easy Connect), + // which also supports pre-defined DSNs. + if (empty($this->hostname) && empty($this->port)) + { + $this->dsn .= $this->database; + } + else + { + $this->dsn .= '//'.(empty($this->hostname) ? '127.0.0.1' : $this->hostname) + .(empty($this->port) ? '' : ':'.$this->port).'/'; + + empty($this->database) OR $this->dsn .= $this->database; + } + + empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set; + } + elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 4) === FALSE) + { + $this->dsn .= ';charset='.$this->char_set; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + $version_string = parent::version(); + if (preg_match('#(Release\s)?(?\d+(?:\.\d+)+)#', $version_string, $match)) + { + return $this->data_cache['version'] = $match['version']; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"'; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (strpos($table, '.') !== FALSE) + { + sscanf($table, '%[^.].%s', $owner, $table); + } + else + { + $owner = $this->username; + } + + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE + FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).' + AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + + $length = ($query[$i]->CHAR_LENGTH > 0) + ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION; + if ($length === NULL) + { + $length = $query[$i]->DATA_LENGTH; + } + $retval[$i]->max_length = $length; + + $default = $query[$i]->DATA_DEFAULT; + if ($default === NULL && $query[$i]->NULLABLE === 'N') + { + $default = ''; + } + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _insert_batch($table, $keys, $values) + { + $keys = implode(', ', $keys); + $sql = "INSERT ALL\n"; + + for ($i = 0, $c = count($values); $i < $c; $i++) + { + $sql .= ' INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n"; + } + + return $sql.'SELECT * FROM dual'; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + $this->where('rownum <= ',$this->qb_limit, FALSE); + $this->qb_limit = FALSE; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + if (version_compare($this->version(), '12.1', '>=')) + { + // OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')' + .($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1): ''); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php b/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php new file mode 100644 index 0000000..0783cd5 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php @@ -0,0 +1,208 @@ +db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + $field[$i] = "\n\t".$field[$i]['_literal']; + } + else + { + $field[$i]['_literal'] = "\n\t".$this->_process_column($field[$i]); + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + + if ($alter_type === 'MODIFY' && ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + } + } + + $sql .= ' '.$alter_type.' '; + $sql .= (count($field) === 1) + ? $field[0] + : '('.implode(',', $field).')'; + + // RENAME COLUMN must be executed after MODIFY + array_unshift($sqls, $sql); + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'number') !== FALSE && version_compare($this->db->version(), '12.1', '>=')) + { + $field['auto_increment'] = ' GENERATED ALWAYS AS IDENTITY'; + } + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['default'] + .$field['auto_increment'] + .$field['null'] + .$field['unique']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'INT': + $attributes['TYPE'] = 'NUMBER'; + return; + case 'BIGINT': + $attributes['TYPE'] = 'NUMBER'; + return; + default: return; + } + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php b/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php new file mode 100644 index 0000000..6b7f237 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php @@ -0,0 +1,230 @@ +dsn)) + { + $this->dsn = 'odbc:'; + + // Pre-defined DSN + if (empty($this->hostname) && empty($this->HOSTNAME) && empty($this->port) && empty($this->PORT)) + { + if (isset($this->DSN)) + { + $this->dsn .= 'DSN='.$this->DSN; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DSN='.$this->database; + } + + return; + } + + // If the DSN is not pre-configured - try to build an IBM DB2 connection string + $this->dsn .= 'DRIVER='.(isset($this->DRIVER) ? '{'.$this->DRIVER.'}' : '{IBM DB2 ODBC DRIVER}').';'; + + if (isset($this->DATABASE)) + { + $this->dsn .= 'DATABASE='.$this->DATABASE.';'; + } + elseif ( ! empty($this->database)) + { + $this->dsn .= 'DATABASE='.$this->database.';'; + } + + if (isset($this->HOSTNAME)) + { + $this->dsn .= 'HOSTNAME='.$this->HOSTNAME.';'; + } + else + { + $this->dsn .= 'HOSTNAME='.(empty($this->hostname) ? '127.0.0.1;' : $this->hostname.';'); + } + + if (isset($this->PORT)) + { + $this->dsn .= 'PORT='.$this->port.';'; + } + elseif ( ! empty($this->port)) + { + $this->dsn .= ';PORT='.$this->port.';'; + } + + $this->dsn .= 'PROTOCOL='.(isset($this->PROTOCOL) ? $this->PROTOCOL.';' : 'TCPIP;'); + } + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + $this->display_error('db_unsupported_feature'); + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '".$this->schema."'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql." AND table_name LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT column_name FROM information_schema.columns WHERE table_name = '.$this->escape($table); + } +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php b/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php new file mode 100644 index 0000000..c9b8238 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php @@ -0,0 +1,71 @@ +dsn)) + { + $this->dsn = 'pgsql:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ';port='.$this->port; + empty($this->database) OR $this->dsn .= ';dbname='.$this->database; + + if ( ! empty($this->username)) + { + $this->dsn .= ';user='.$this->username; + empty($this->password) OR $this->dsn .= ';password='.$this->password; + } + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + $this->conn_id = parent::db_connect($persistent); + + if (is_object($this->conn_id) && ! empty($this->schema)) + { + $this->simple_query('SET search_path TO '.$this->schema.',public'); + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @param string $name + * @return int + */ + public function insert_id($name = NULL) + { + if ($name === NULL && version_compare($this->version(), '8.1', '>=')) + { + $query = $this->query('SELECT LASTVAL() AS ins_id'); + $query = $query->row(); + return $query->ins_id; + } + + return $this->conn_id->lastInsertId($name); + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * "Smart" Escape String + * + * Escapes data based on type + * + * @param string $str + * @return mixed + */ + public function escape($str) + { + if (is_bool($str)) + { + return ($str) ? 'TRUE' : 'FALSE'; + } + + return parent::escape($str); + } + + // -------------------------------------------------------------------- + + /** + * ORDER BY + * + * @param string $orderby + * @param string $direction ASC, DESC or RANDOM + * @param bool $escape + * @return object + */ + public function order_by($orderby, $direction = '', $escape = NULL) + { + $direction = strtoupper(trim($direction)); + if ($direction === 'RANDOM') + { + if ( ! is_float($orderby) && ctype_digit((string) $orderby)) + { + $orderby = ($orderby > 1) + ? (float) '0.'.$orderby + : (float) $orderby; + } + + if (is_float($orderby)) + { + $this->simple_query('SET SEED '.$orderby); + } + + $orderby = $this->_random_keyword[0]; + $direction = ''; + $escape = FALSE; + } + + return parent::order_by($orderby, $direction, $escape); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' AND "table_name" LIKE \'' + .$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT "column_name" + FROM "information_schema"."columns" + WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default" + FROM "information_schema"."columns" + WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->column_name; + $retval[$i]->type = $query[$i]->data_type; + $retval[$i]->max_length = ($query[$i]->character_maximum_length > 0) ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision; + $retval[$i]->default = $query[$i]->column_default; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]['value']; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$val[$field]['field']][] = 'WHEN '.$val[$index]['value'].' THEN '.$val[$field]['value']; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k.' = (CASE '.$val[$index]['field']."\n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END), '; + } + + $this->where($val[$index]['field'].' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : ''); + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php new file mode 100644 index 0000000..cea2054 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php @@ -0,0 +1,218 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INT4' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'INT8' => 'NUMERIC', + 'BIGINT' => 'NUMERIC', + 'REAL' => 'DOUBLE PRECISION', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @param object &$db Database object + * @return void + */ + public function __construct(&$db) + { + parent::__construct($db); + + if (version_compare($this->db->version(), '9.0', '>')) + { + $this->create_table_if = 'CREATE TABLE IF NOT EXISTS'; + } + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .(trim($field[$i]['null']) === $this->_null ? ' DROP NOT NULL' : ' SET NOT NULL'); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + // Reset field lengths for data types that don't support it + if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE) + { + $attributes['CONSTRAINT'] = NULL; + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE) + { + $field['type'] = ($field['type'] === 'NUMERIC') + ? 'BIGSERIAL' + : 'SERIAL'; + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php b/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php new file mode 100644 index 0000000..24c34f2 --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php @@ -0,0 +1,214 @@ +dsn)) + { + $this->dsn = 'sqlite:'; + + if (empty($this->database) && empty($this->hostname)) + { + $this->database = ':memory:'; + } + + $this->database = empty($this->database) ? $this->hostname : $this->database; + } + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + return $sql.' AND "NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * @param string $table Table name + * @return array + */ + public function list_fields($table) + { + if (($result = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $fields = array(); + foreach ($result->result_array() as $row) + { + $fields[] = $row['name']; + } + + return $fields; + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $query = $query->result_array(); + if (empty($query)) + { + return FALSE; + } + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]['name']; + $retval[$i]->type = $query[$i]['type']; + $retval[$i]->max_length = NULL; + $retval[$i]->default = $query[$i]['dflt_value']; + $retval[$i]->primary_key = isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'INSERT OR '.parent::_replace($table, $keys, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php b/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php new file mode 100644 index 0000000..b0edcbd --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php @@ -0,0 +1,239 @@ +db->version(), '3.3', '<')) + { + $this->_create_table_if = FALSE; + $this->_drop_table_if = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name (ignored) + * @return bool + */ + public function create_database($db_name) + { + // In SQLite, a database is created when you connect to the database. + // We'll return TRUE so that an error isn't generated + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + // In SQLite, a database is dropped when we delete a file + if (file_exists($this->db->database)) + { + // We need to close the pseudo-connection first + $this->db->close(); + if ( ! @unlink($this->db->database)) + { + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP' OR $alter_type === 'CHANGE') + { + // drop_column(): + // BEGIN TRANSACTION; + // CREATE TEMPORARY TABLE t1_backup(a,b); + // INSERT INTO t1_backup SELECT a,b FROM t1; + // DROP TABLE t1; + // CREATE TABLE t1(a,b); + // INSERT INTO t1 SELECT a,b FROM t1_backup; + // DROP TABLE t1_backup; + // COMMIT; + + return FALSE; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primary_keys = array(); + } + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php new file mode 100644 index 0000000..685b61e --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php @@ -0,0 +1,370 @@ +dsn)) + { + $this->dsn = 'sqlsrv:Server='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname); + + empty($this->port) OR $this->dsn .= ','.$this->port; + empty($this->database) OR $this->dsn .= ';Database='.$this->database; + + // Some custom options + + if (isset($this->QuotedId)) + { + $this->dsn .= ';QuotedId='.$this->QuotedId; + $this->_quoted_identifier = (bool) $this->QuotedId; + } + + if (isset($this->ConnectionPooling)) + { + $this->dsn .= ';ConnectionPooling='.$this->ConnectionPooling; + } + + if ($this->encrypt === TRUE) + { + $this->dsn .= ';Encrypt=1'; + } + + if (isset($this->TraceOn)) + { + $this->dsn .= ';TraceOn='.$this->TraceOn; + } + + if (isset($this->TrustServerCertificate)) + { + $this->dsn .= ';TrustServerCertificate='.$this->TrustServerCertificate; + } + + empty($this->APP) OR $this->dsn .= ';APP='.$this->APP; + empty($this->Failover_Partner) OR $this->dsn .= ';Failover_Partner='.$this->Failover_Partner; + empty($this->LoginTimeout) OR $this->dsn .= ';LoginTimeout='.$this->LoginTimeout; + empty($this->MultipleActiveResultSets) OR $this->dsn .= ';MultipleActiveResultSets='.$this->MultipleActiveResultSets; + empty($this->TraceFile) OR $this->dsn .= ';TraceFile='.$this->TraceFile; + empty($this->WSID) OR $this->dsn .= ';WSID='.$this->WSID; + } + elseif (preg_match('/QuotedId=(0|1)/', $this->dsn, $match)) + { + $this->_quoted_identifier = (bool) $match[1]; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return object + */ + public function db_connect($persistent = FALSE) + { + if ( ! empty($this->char_set) && preg_match('/utf[^8]*8/i', $this->char_set)) + { + $this->options[PDO::SQLSRV_ENCODING_UTF8] = 1; + } + + $this->conn_id = parent::db_connect($persistent); + + if ( ! is_object($this->conn_id) OR is_bool($this->_quoted_identifier)) + { + return $this->conn_id; + } + + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // As of SQL Server 2012 (11.0.*) OFFSET is supported + if (version_compare($this->version(), '11', '>=')) + { + // SQL Server OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + $limit = $this->qb_offset + $this->qb_limit; + + // An ORDER BY clause is required for ROW_NUMBER() to work + if ($this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } + +} diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php new file mode 100644 index 0000000..07eecea --- /dev/null +++ b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php @@ -0,0 +1,150 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/postgre/index.html b/system/database/drivers/postgre/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/postgre/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/postgre/postgre_driver.php b/system/database/drivers/postgre/postgre_driver.php new file mode 100644 index 0000000..15d800b --- /dev/null +++ b/system/database/drivers/postgre/postgre_driver.php @@ -0,0 +1,611 @@ +dsn === '' OR $this->dsn = ''; + + if (strpos($this->hostname, '/') !== FALSE) + { + // If UNIX sockets are used, we shouldn't set a port + $this->port = ''; + } + + $this->hostname === '' OR $this->dsn = 'host='.$this->hostname.' '; + + if ( ! empty($this->port) && ctype_digit($this->port)) + { + $this->dsn .= 'port='.$this->port.' '; + } + + if ($this->username !== '') + { + $this->dsn .= 'user='.$this->username.' '; + + /* An empty password is valid! + * + * $db['password'] = NULL must be done in order to ignore it. + */ + $this->password === NULL OR $this->dsn .= "password='".$this->password."' "; + } + + $this->database === '' OR $this->dsn .= 'dbname='.$this->database.' '; + + /* We don't have these options as elements in our standard configuration + * array, but they might be set by parse_url() if the configuration was + * provided via string. Example: + * + * postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1 + */ + foreach (array('connect_timeout', 'options', 'sslmode', 'service') as $key) + { + if (isset($this->$key) && is_string($this->$key) && $this->$key !== '') + { + $this->dsn .= $key."='".$this->$key."' "; + } + } + + $this->dsn = rtrim($this->dsn); + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $persistent + * @return resource|object + */ + public function db_connect($persistent = FALSE) + { + empty($this->dsn) && $this->_build_dsn(); + $this->conn_id = ($persistent === TRUE) + ? pg_pconnect($this->dsn) + : pg_connect($this->dsn); + + if ($this->conn_id !== FALSE) + { + if ($persistent === TRUE + && pg_connection_status($this->conn_id) === PGSQL_CONNECTION_BAD + && pg_ping($this->conn_id) === FALSE + ) + { + return FALSE; + } + + empty($this->schema) OR $this->simple_query('SET search_path TO '.$this->schema.',public'); + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Reconnect + * + * Keep / reestablish the db connection if no queries have been + * sent for a length of time exceeding the server's idle timeout + * + * @return void + */ + public function reconnect() + { + if (pg_ping($this->conn_id) === FALSE) + { + $this->conn_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Set client character set + * + * @param string $charset + * @return bool + */ + protected function _db_set_charset($charset) + { + return (pg_set_client_encoding($this->conn_id, $charset) === 0); + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($pg_version = pg_version($this->conn_id)) === FALSE) + { + return FALSE; + } + + /* If PHP was compiled with PostgreSQL lib versions earlier + * than 7.4, pg_version() won't return the server version + * and so we'll have to fall back to running a query in + * order to get it. + */ + return (isset($pg_version['server']) && preg_match('#^(\d+\.\d+)#', $pg_version['server'], $match)) + ? $this->data_cache['version'] = $match[1] + : parent::version(); + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource|object + */ + protected function _execute($sql) + { + return pg_query($this->conn_id, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return (bool) pg_query($this->conn_id, 'BEGIN'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return (bool) pg_query($this->conn_id, 'COMMIT'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return (bool) pg_query($this->conn_id, 'ROLLBACK'); + } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * @param string An SQL query string + * @return bool + */ + public function is_write_type($sql) + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return FALSE; + } + + return parent::is_write_type($sql); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return pg_escape_string($this->conn_id, $str); + } + + // -------------------------------------------------------------------- + + /** + * "Smart" Escape String + * + * Escapes data based on type + * + * @param string $str + * @return mixed + */ + public function escape($str) + { + if (is_php('5.4.4') && (is_string($str) OR (is_object($str) && method_exists($str, '__toString')))) + { + return pg_escape_literal($this->conn_id, $str); + } + elseif (is_bool($str)) + { + return ($str) ? 'TRUE' : 'FALSE'; + } + + return parent::escape($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return pg_affected_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return string + */ + public function insert_id() + { + $v = $this->version(); + + $table = (func_num_args() > 0) ? func_get_arg(0) : NULL; + $column = (func_num_args() > 1) ? func_get_arg(1) : NULL; + + if ($table === NULL && $v >= '8.1') + { + $sql = 'SELECT LASTVAL() AS ins_id'; + } + elseif ($table !== NULL) + { + if ($column !== NULL && $v >= '8.0') + { + $sql = 'SELECT pg_get_serial_sequence(\''.$table."', '".$column."') AS seq"; + $query = $this->query($sql); + $query = $query->row(); + $seq = $query->seq; + } + else + { + // seq_name passed in table parameter + $seq = $table; + } + + $sql = 'SELECT CURRVAL(\''.$seq."') AS ins_id"; + } + else + { + return pg_last_oid($this->result_id); + } + + $query = $this->query($sql); + $query = $query->row(); + return (int) $query->ins_id; + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'"; + + if ($prefix_limit !== FALSE && $this->dbprefix !== '') + { + return $sql.' AND "table_name" LIKE \'' + .$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT "column_name" + FROM "information_schema"."columns" + WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default" + FROM "information_schema"."columns" + WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->column_name; + $retval[$i]->type = $query[$i]->data_type; + $retval[$i]->max_length = ($query[$i]->character_maximum_length > 0) ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision; + $retval[$i]->default = $query[$i]->column_default; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + return array('code' => '', 'message' => pg_last_error($this->conn_id)); + } + + // -------------------------------------------------------------------- + + /** + * ORDER BY + * + * @param string $orderby + * @param string $direction ASC, DESC or RANDOM + * @param bool $escape + * @return object + */ + public function order_by($orderby, $direction = '', $escape = NULL) + { + $direction = strtoupper(trim($direction)); + if ($direction === 'RANDOM') + { + if ( ! is_float($orderby) && ctype_digit((string) $orderby)) + { + $orderby = ($orderby > 1) + ? (float) '0.'.$orderby + : (float) $orderby; + } + + if (is_float($orderby)) + { + $this->simple_query('SET SEED '.$orderby); + } + + $orderby = $this->_random_keyword[0]; + $direction = ''; + $escape = FALSE; + } + + return parent::order_by($orderby, $direction, $escape); + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Update_Batch statement + * + * Generates a platform-specific batch update string from the supplied data + * + * @param string $table Table name + * @param array $values Update data + * @param string $index WHERE key + * @return string + */ + protected function _update_batch($table, $values, $index) + { + $ids = array(); + foreach ($values as $key => $val) + { + $ids[] = $val[$index]['value']; + + foreach (array_keys($val) as $field) + { + if ($field !== $index) + { + $final[$val[$field]['field']][] = 'WHEN '.$val[$index]['value'].' THEN '.$val[$field]['value']; + } + } + } + + $cases = ''; + foreach ($final as $k => $v) + { + $cases .= $k.' = (CASE '.$val[$index]['field']."\n" + .implode("\n", $v)."\n" + .'ELSE '.$k.' END), '; + } + + $this->where($val[$index]['field'].' IN('.implode(',', $ids).')', NULL, FALSE); + + return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where'); + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + $this->qb_limit = FALSE; + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : ''); + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + pg_close($this->conn_id); + } + +} diff --git a/system/database/drivers/postgre/postgre_forge.php b/system/database/drivers/postgre/postgre_forge.php new file mode 100644 index 0000000..2857fd5 --- /dev/null +++ b/system/database/drivers/postgre/postgre_forge.php @@ -0,0 +1,206 @@ + 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INT4' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'INT8' => 'NUMERIC', + 'BIGINT' => 'NUMERIC', + 'REAL' => 'DOUBLE PRECISION', + 'FLOAT' => 'DOUBLE PRECISION' + ); + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + // -------------------------------------------------------------------- + + /** + * Class constructor + * + * @param object &$db Database object + * @return void + */ + public function __construct(&$db) + { + parent::__construct($db); + + if (version_compare($this->db->version(), '9.0', '>')) + { + $this->create_table_if = 'CREATE TABLE IF NOT EXISTS'; + } + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('DROP', 'ADD'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table); + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + if ($field[$i]['_literal'] !== FALSE) + { + return FALSE; + } + + if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TYPE '.$field[$i]['type'].$field[$i]['length']; + } + + if ( ! empty($field[$i]['default'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' SET '.$field[$i]['default']; + } + + if (isset($field[$i]['null'])) + { + $sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .(trim($field[$i]['null']) === $this->_null ? ' DROP NOT NULL' : ' SET NOT NULL'); + } + + if ( ! empty($field[$i]['new_name'])) + { + $sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name']) + .' TO '.$this->db->escape_identifiers($field[$i]['new_name']); + } + + if ( ! empty($field[$i]['comment'])) + { + $sqls[] = 'COMMENT ON COLUMN ' + .$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name']) + .' IS '.$field[$i]['comment']; + } + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + // Reset field lengths for data types that don't support it + if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE) + { + $attributes['CONSTRAINT'] = NULL; + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'TINYINT': + $attributes['TYPE'] = 'SMALLINT'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE) + { + $field['type'] = ($field['type'] === 'NUMERIC') + ? 'BIGSERIAL' + : 'SERIAL'; + } + } + +} diff --git a/system/database/drivers/postgre/postgre_result.php b/system/database/drivers/postgre/postgre_result.php new file mode 100644 index 0000000..5e4145e --- /dev/null +++ b/system/database/drivers/postgre/postgre_result.php @@ -0,0 +1,183 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = pg_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return pg_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field_names[] = pg_field_name($this->result_id, $i); + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = pg_field_name($this->result_id, $i); + $retval[$i]->type = pg_field_type($this->result_id, $i); + $retval[$i]->max_length = pg_field_size($this->result_id, $i); + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if ($this->result_id !== FALSE) + { + pg_free_result($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return pg_result_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return pg_fetch_assoc($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return pg_fetch_object($this->result_id, NULL, $class_name); + } + +} diff --git a/system/database/drivers/postgre/postgre_utility.php b/system/database/drivers/postgre/postgre_utility.php new file mode 100644 index 0000000..c8356d5 --- /dev/null +++ b/system/database/drivers/postgre/postgre_utility.php @@ -0,0 +1,79 @@ +db->display_error('db_unsupported_feature'); + } +} diff --git a/system/database/drivers/sqlite/index.html b/system/database/drivers/sqlite/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/sqlite/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/sqlite/sqlite_driver.php b/system/database/drivers/sqlite/sqlite_driver.php new file mode 100644 index 0000000..188f00c --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_driver.php @@ -0,0 +1,331 @@ +database, 0666, $error) + : sqlite_open($this->database, 0666, $error); + + isset($error) && log_message('error', $error); + + return $conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + return isset($this->data_cache['version']) + ? $this->data_cache['version'] + : $this->data_cache['version'] = sqlite_libversion(); + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return $this->is_write_type($sql) + ? sqlite_exec($this->conn_id, $sql) + : sqlite_query($this->conn_id, $sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->simple_query('BEGIN TRANSACTION'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->simple_query('COMMIT'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->simple_query('ROLLBACK'); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return sqlite_escape_string($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return sqlite_changes($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return sqlite_last_insert_rowid($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = "SELECT name FROM sqlite_master WHERE type='table'"; + + if ($prefix_limit !== FALSE && $this->dbprefix != '') + { + return $sql." AND 'name' LIKE '".$this->escape_like_str($this->dbprefix)."%' ".sprintf($this->_like_escape_str, $this->_like_escape_chr); + } + + return $sql; + } + + // -------------------------------------------------------------------- + + /** + * Show column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return bool + */ + protected function _list_columns($table = '') + { + // Not supported + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $query = $query->result_array(); + if (empty($query)) + { + return FALSE; + } + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]['name']; + $retval[$i]->type = $query[$i]['type']; + $retval[$i]->max_length = NULL; + $retval[$i]->default = $query[$i]['dflt_value']; + $retval[$i]->primary_key = isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occured. + * + * @return array + */ + public function error() + { + $error = array('code' => sqlite_last_error($this->conn_id)); + $error['message'] = sqlite_error_string($error['code']); + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * Generates a platform-specific replace string from the supplied data + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'INSERT OR '.parent::_replace($table, $keys, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this function maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + sqlite_close($this->conn_id); + } + +} diff --git a/system/database/drivers/sqlite/sqlite_forge.php b/system/database/drivers/sqlite/sqlite_forge.php new file mode 100644 index 0000000..60aaa09 --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_forge.php @@ -0,0 +1,206 @@ +db->database) OR ! @unlink($this->db->database)) + { + return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP' OR $alter_type === 'CHANGE') + { + // drop_column(): + // BEGIN TRANSACTION; + // CREATE TEMPORARY TABLE t1_backup(a,b); + // INSERT INTO t1_backup SELECT a,b FROM t1; + // DROP TABLE t1; + // CREATE TABLE t1(a,b); + // INSERT INTO t1 SELECT a,b FROM t1_backup; + // DROP TABLE t1_backup; + // COMMIT; + + return FALSE; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primary_keys = array(); + } + } + +} diff --git a/system/database/drivers/sqlite/sqlite_result.php b/system/database/drivers/sqlite/sqlite_result.php new file mode 100644 index 0000000..1df9025 --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_result.php @@ -0,0 +1,165 @@ +num_rows) + ? $this->num_rows + : $this->num_rows = @sqlite_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return @sqlite_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field_names[$i] = sqlite_field_name($this->result_id, $i); + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = sqlite_field_name($this->result_id, $i); + $retval[$i]->type = NULL; + $retval[$i]->max_length = NULL; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n + * @return bool + */ + public function data_seek($n = 0) + { + return sqlite_seek($this->result_id, $n); + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return sqlite_fetch_array($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return sqlite_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/sqlite/sqlite_utility.php b/system/database/drivers/sqlite/sqlite_utility.php new file mode 100644 index 0000000..5f9adf2 --- /dev/null +++ b/system/database/drivers/sqlite/sqlite_utility.php @@ -0,0 +1,62 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/sqlite3/index.html b/system/database/drivers/sqlite3/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/sqlite3/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/sqlite3/sqlite3_driver.php b/system/database/drivers/sqlite3/sqlite3_driver.php new file mode 100644 index 0000000..be79ddd --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_driver.php @@ -0,0 +1,345 @@ +password) + ? new SQLite3($this->database) + : new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password); + } + catch (Exception $e) + { + return FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + $version = SQLite3::version(); + return $this->data_cache['version'] = $version['versionString']; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @todo Implement use of SQLite3::querySingle(), if needed + * @param string $sql + * @return mixed SQLite3Result object or bool + */ + protected function _execute($sql) + { + return $this->is_write_type($sql) + ? $this->conn_id->exec($sql) + : $this->conn_id->query($sql); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return $this->conn_id->exec('BEGIN TRANSACTION'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return $this->conn_id->exec('END TRANSACTION'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return $this->conn_id->exec('ROLLBACK'); + } + + // -------------------------------------------------------------------- + + /** + * Platform-dependent string escape + * + * @param string + * @return string + */ + protected function _escape_str($str) + { + return $this->conn_id->escapeString($str); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return $this->conn_id->changes(); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insert_id() + { + return $this->conn_id->lastInsertRowID(); + } + + // -------------------------------------------------------------------- + + /** + * Show table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool $prefix_limit + * @return string + */ + protected function _list_tables($prefix_limit = FALSE) + { + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + .(($prefix_limit !== FALSE && $this->dbprefix != '') + ? ' AND "NAME" LIKE \''.$this->escape_like_str($this->dbprefix).'%\' '.sprintf($this->_like_escape_str, $this->_like_escape_chr) + : ''); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * @param string $table Table name + * @return array + */ + public function list_fields($table) + { + if (($result = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $fields = array(); + foreach ($result->result_array() as $row) + { + $fields[] = $row['name']; + } + + return $fields; + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return FALSE; + } + + $query = $query->result_array(); + if (empty($query)) + { + return FALSE; + } + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]['name']; + $retval[$i]->type = $query[$i]['type']; + $retval[$i]->max_length = NULL; + $retval[$i]->default = $query[$i]['dflt_value']; + $retval[$i]->primary_key = isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + return array('code' => $this->conn_id->lastErrorCode(), 'message' => $this->conn_id->lastErrorMsg()); + } + + // -------------------------------------------------------------------- + + /** + * Replace statement + * + * Generates a platform-specific replace string from the supplied data + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string + */ + protected function _replace($table, $keys, $values) + { + return 'INSERT OR '.parent::_replace($table, $keys, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'DELETE FROM '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + $this->conn_id->close(); + } + +} diff --git a/system/database/drivers/sqlite3/sqlite3_forge.php b/system/database/drivers/sqlite3/sqlite3_forge.php new file mode 100644 index 0000000..5658b3e --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_forge.php @@ -0,0 +1,226 @@ +db->version(), '3.3', '<')) + { + $this->_create_table_if = FALSE; + $this->_drop_table_if = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * @return bool + */ + public function create_database($db_name) + { + // In SQLite, a database is created when you connect to the database. + // We'll return TRUE so that an error isn't generated + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name (ignored) + * @return bool + */ + public function drop_database($db_name) + { + // In SQLite, a database is dropped when we delete a file + if (file_exists($this->db->database)) + { + // We need to close the pseudo-connection first + $this->db->close(); + if ( ! @unlink($this->db->database)) + { + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + elseif ( ! empty($this->db->data_cache['db_names'])) + { + $key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE); + if ($key !== FALSE) + { + unset($this->db->data_cache['db_names'][$key]); + } + } + + return TRUE; + } + + return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if ($alter_type === 'DROP' OR $alter_type === 'CHANGE') + { + // drop_column(): + // BEGIN TRANSACTION; + // CREATE TEMPORARY TABLE t1_backup(a,b); + // INSERT INTO t1_backup SELECT a,b FROM t1; + // DROP TABLE t1; + // CREATE TABLE t1(a,b); + // INSERT INTO t1 SELECT a,b FROM t1_backup; + // DROP TABLE t1_backup; + // COMMIT; + + return FALSE; + } + + return parent::_alter_table($alter_type, $table, $field); + } + + // -------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _process_column($field) + { + return $this->db->escape_identifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primary_keys = array(); + } + } + +} diff --git a/system/database/drivers/sqlite3/sqlite3_result.php b/system/database/drivers/sqlite3/sqlite3_result.php new file mode 100644 index 0000000..47fe9d2 --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_result.php @@ -0,0 +1,195 @@ +result_id->numColumns(); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $field_names[] = $this->result_id->columnName($i); + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + static $data_types = array( + SQLITE3_INTEGER => 'integer', + SQLITE3_FLOAT => 'float', + SQLITE3_TEXT => 'text', + SQLITE3_BLOB => 'blob', + SQLITE3_NULL => 'null' + ); + + $retval = array(); + for ($i = 0, $c = $this->num_fields(); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $this->result_id->columnName($i); + + $type = $this->result_id->columnType($i); + $retval[$i]->type = isset($data_types[$type]) ? $data_types[$type] : $type; + + $retval[$i]->max_length = NULL; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_object($this->result_id)) + { + $this->result_id->finalize(); + $this->result_id = NULL; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return $this->result_id->fetchArray(SQLITE3_ASSOC); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + // No native support for fetching rows as objects + if (($row = $this->result_id->fetchArray(SQLITE3_ASSOC)) === FALSE) + { + return FALSE; + } + elseif ($class_name === 'stdClass') + { + return (object) $row; + } + + $class_name = new $class_name(); + foreach (array_keys($row) as $key) + { + $class_name->$key = $row[$key]; + } + + return $class_name; + } + + // -------------------------------------------------------------------- + + /** + * Data Seek + * + * Moves the internal pointer to the desired offset. We call + * this internally before fetching results to make sure the + * result set starts at zero. + * + * @param int $n (ignored) + * @return array + */ + public function data_seek($n = 0) + { + // Only resetting to the start of the result set is supported + return ($n > 0) ? FALSE : $this->result_id->reset(); + } + +} diff --git a/system/database/drivers/sqlite3/sqlite3_utility.php b/system/database/drivers/sqlite3/sqlite3_utility.php new file mode 100644 index 0000000..90316bc --- /dev/null +++ b/system/database/drivers/sqlite3/sqlite3_utility.php @@ -0,0 +1,62 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/drivers/sqlsrv/index.html b/system/database/drivers/sqlsrv/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/drivers/sqlsrv/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/database/drivers/sqlsrv/sqlsrv_driver.php b/system/database/drivers/sqlsrv/sqlsrv_driver.php new file mode 100644 index 0000000..7877794 --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_driver.php @@ -0,0 +1,544 @@ +scrollable === NULL) + { + $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') + ? SQLSRV_CURSOR_CLIENT_BUFFERED + : FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Database connection + * + * @param bool $pooling + * @return resource + */ + public function db_connect($pooling = FALSE) + { + $charset = in_array(strtolower($this->char_set), array('utf-8', 'utf8'), TRUE) + ? 'UTF-8' : SQLSRV_ENC_CHAR; + + $connection = array( + 'UID' => empty($this->username) ? '' : $this->username, + 'PWD' => empty($this->password) ? '' : $this->password, + 'Database' => $this->database, + 'ConnectionPooling' => ($pooling === TRUE) ? 1 : 0, + 'CharacterSet' => $charset, + 'Encrypt' => ($this->encrypt === TRUE) ? 1 : 0, + 'ReturnDatesAsStrings' => 1 + ); + + // If the username and password are both empty, assume this is a + // 'Windows Authentication Mode' connection. + if (empty($connection['UID']) && empty($connection['PWD'])) + { + unset($connection['UID'], $connection['PWD']); + } + + if (FALSE !== ($this->conn_id = sqlsrv_connect($this->hostname, $connection))) + { + // Determine how identifiers are escaped + $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); + $query = $query->row_array(); + $this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi']; + $this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']'); + } + + return $this->conn_id; + } + + // -------------------------------------------------------------------- + + /** + * Select the database + * + * @param string $database + * @return bool + */ + public function db_select($database = '') + { + if ($database === '') + { + $database = $this->database; + } + + if ($this->_execute('USE '.$this->escape_identifiers($database))) + { + $this->database = $database; + $this->data_cache = array(); + return TRUE; + } + + return FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Execute the query + * + * @param string $sql an SQL query + * @return resource + */ + protected function _execute($sql) + { + return ($this->scrollable === FALSE OR $this->is_write_type($sql)) + ? sqlsrv_query($this->conn_id, $sql) + : sqlsrv_query($this->conn_id, $sql, NULL, array('Scrollable' => $this->scrollable)); + } + + // -------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _trans_begin() + { + return sqlsrv_begin_transaction($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _trans_commit() + { + return sqlsrv_commit($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _trans_rollback() + { + return sqlsrv_rollback($this->conn_id); + } + + // -------------------------------------------------------------------- + + /** + * Affected Rows + * + * @return int + */ + public function affected_rows() + { + return sqlsrv_rows_affected($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Insert ID + * + * Returns the last id created in the Identity column. + * + * @return string + */ + public function insert_id() + { + return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->row()->insert_id; + } + + // -------------------------------------------------------------------- + + /** + * Database version number + * + * @return string + */ + public function version() + { + if (isset($this->data_cache['version'])) + { + return $this->data_cache['version']; + } + + if ( ! $this->conn_id OR ($info = sqlsrv_server_info($this->conn_id)) === FALSE) + { + return FALSE; + } + + return $this->data_cache['version'] = $info['SQLServerVersion']; + } + + // -------------------------------------------------------------------- + + /** + * List table query + * + * Generates a platform-specific query string so that the table names can be fetched + * + * @param bool + * @return string $prefix_limit + */ + protected function _list_tables($prefix_limit = FALSE) + { + $sql = 'SELECT '.$this->escape_identifiers('name') + .' FROM '.$this->escape_identifiers('sysobjects') + .' WHERE '.$this->escape_identifiers('type')." = 'U'"; + + if ($prefix_limit === TRUE && $this->dbprefix !== '') + { + $sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' " + .sprintf($this->_escape_like_str, $this->_escape_like_chr); + } + + return $sql.' ORDER BY '.$this->escape_identifiers('name'); + } + + // -------------------------------------------------------------------- + + /** + * List column query + * + * Generates a platform-specific query string so that the column names can be fetched + * + * @param string $table + * @return string + */ + protected function _list_columns($table = '') + { + return 'SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + } + + // -------------------------------------------------------------------- + + /** + * Returns an object with field data + * + * @param string $table + * @return array + */ + public function field_data($table) + { + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT + FROM INFORMATION_SCHEMA.Columns + WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === FALSE) + { + return FALSE; + } + $query = $query->result_object(); + + $retval = array(); + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + $retval[$i]->max_length = ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION; + $retval[$i]->default = $query[$i]->COLUMN_DEFAULT; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Error + * + * Returns an array containing code and message of the last + * database error that has occurred. + * + * @return array + */ + public function error() + { + $error = array('code' => '00000', 'message' => ''); + $sqlsrv_errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); + + if ( ! is_array($sqlsrv_errors)) + { + return $error; + } + + $sqlsrv_error = array_shift($sqlsrv_errors); + if (isset($sqlsrv_error['SQLSTATE'])) + { + $error['code'] = isset($sqlsrv_error['code']) ? $sqlsrv_error['SQLSTATE'].'/'.$sqlsrv_error['code'] : $sqlsrv_error['SQLSTATE']; + } + elseif (isset($sqlsrv_error['code'])) + { + $error['code'] = $sqlsrv_error['code']; + } + + if (isset($sqlsrv_error['message'])) + { + $error['message'] = $sqlsrv_error['message']; + } + + return $error; + } + + // -------------------------------------------------------------------- + + /** + * Update statement + * + * Generates a platform-specific update string from the supplied data + * + * @param string $table + * @param array $values + * @return string + */ + protected function _update($table, $values) + { + $this->qb_limit = FALSE; + $this->qb_orderby = array(); + return parent::_update($table, $values); + } + + // -------------------------------------------------------------------- + + /** + * Truncate statement + * + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the TRUNCATE statement, + * then this method maps to 'DELETE FROM table' + * + * @param string $table + * @return string + */ + protected function _truncate($table) + { + return 'TRUNCATE TABLE '.$table; + } + + // -------------------------------------------------------------------- + + /** + * Delete statement + * + * Generates a platform-specific delete string from the supplied data + * + * @param string $table + * @return string + */ + protected function _delete($table) + { + if ($this->qb_limit) + { + return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete'; + } + + return parent::_delete($table); + } + + // -------------------------------------------------------------------- + + /** + * LIMIT + * + * Generates a platform-specific LIMIT clause + * + * @param string $sql SQL Query + * @return string + */ + protected function _limit($sql) + { + // As of SQL Server 2012 (11.0.*) OFFSET is supported + if (version_compare($this->version(), '11', '>=')) + { + // SQL Server OFFSET-FETCH can be used only with the ORDER BY clause + empty($this->qb_orderby) && $sql .= ' ORDER BY 1'; + + return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY'; + } + + $limit = $this->qb_offset + $this->qb_limit; + + // An ORDER BY clause is required for ROW_NUMBER() to work + if ($this->qb_offset && ! empty($this->qb_orderby)) + { + $orderby = $this->_compile_order_by(); + + // We have to strip the ORDER BY clause + $sql = trim(substr($sql, 0, strrpos($sql, $orderby))); + + // Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results + if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE) + { + $select = '*'; // Inevitable + } + else + { + // Use only field names and their aliases, everything else is out of our scope. + $select = array(); + $field_regexp = ($this->_quoted_identifier) + ? '("[^\"]+")' : '(\[[^\]]+\])'; + for ($i = 0, $c = count($this->qb_select); $i < $c; $i++) + { + $select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m) + ? $m[1] : $this->qb_select[$i]; + } + $select = implode(', ', $select); + } + + return 'SELECT '.$select." FROM (\n\n" + .preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql) + ."\n\n) ".$this->escape_identifiers('CI_subquery') + ."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit; + } + + return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql); + } + + // -------------------------------------------------------------------- + + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * @return string|bool + */ + protected function _insert_batch($table, $keys, $values) + { + // Multiple-value inserts are only supported as of SQL Server 2008 + if (version_compare($this->version(), '10', '>=')) + { + return parent::_insert_batch($table, $keys, $values); + } + + return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE; + } + + // -------------------------------------------------------------------- + + /** + * Close DB Connection + * + * @return void + */ + protected function _close() + { + sqlsrv_close($this->conn_id); + } + +} diff --git a/system/database/drivers/sqlsrv/sqlsrv_forge.php b/system/database/drivers/sqlsrv/sqlsrv_forge.php new file mode 100644 index 0000000..dca7f75 --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_forge.php @@ -0,0 +1,150 @@ + 'SMALLINT', + 'SMALLINT' => 'INT', + 'INT' => 'BIGINT', + 'REAL' => 'FLOAT' + ); + + // -------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * @return string|string[] + */ + protected function _alter_table($alter_type, $table, $field) + { + if (in_array($alter_type, array('ADD', 'DROP'), TRUE)) + { + return parent::_alter_table($alter_type, $table, $field); + } + + $sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN '; + $sqls = array(); + for ($i = 0, $c = count($field); $i < $c; $i++) + { + $sqls[] = $sql.$this->_process_column($field[$i]); + } + + return $sqls; + } + + // -------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * @return void + */ + protected function _attr_type(&$attributes) + { + if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE) + { + unset($attributes['CONSTRAINT']); + } + + switch (strtoupper($attributes['TYPE'])) + { + case 'MEDIUMINT': + $attributes['TYPE'] = 'INTEGER'; + $attributes['UNSIGNED'] = FALSE; + return; + case 'INTEGER': + $attributes['TYPE'] = 'INT'; + return; + default: return; + } + } + + // -------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * @return void + */ + protected function _attr_auto_increment(&$attributes, &$field) + { + if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE) + { + $field['auto_increment'] = ' IDENTITY(1,1)'; + } + } + +} diff --git a/system/database/drivers/sqlsrv/sqlsrv_result.php b/system/database/drivers/sqlsrv/sqlsrv_result.php new file mode 100644 index 0000000..a3a582b --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_result.php @@ -0,0 +1,194 @@ +scrollable = $driver_object->scrollable; + } + + // -------------------------------------------------------------------- + + /** + * Number of rows in the result set + * + * @return int + */ + public function num_rows() + { + // sqlsrv_num_rows() doesn't work with the FORWARD and DYNAMIC cursors (FALSE is the same as FORWARD) + if ( ! in_array($this->scrollable, array(FALSE, SQLSRV_CURSOR_FORWARD, SQLSRV_CURSOR_DYNAMIC), TRUE)) + { + return parent::num_rows(); + } + + return is_int($this->num_rows) + ? $this->num_rows + : $this->num_rows = sqlsrv_num_rows($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Number of fields in the result set + * + * @return int + */ + public function num_fields() + { + return @sqlsrv_num_fields($this->result_id); + } + + // -------------------------------------------------------------------- + + /** + * Fetch Field Names + * + * Generates an array of column names + * + * @return array + */ + public function list_fields() + { + $field_names = array(); + foreach (sqlsrv_field_metadata($this->result_id) as $offset => $field) + { + $field_names[] = $field['Name']; + } + + return $field_names; + } + + // -------------------------------------------------------------------- + + /** + * Field data + * + * Generates an array of objects containing field meta-data + * + * @return array + */ + public function field_data() + { + $retval = array(); + foreach (sqlsrv_field_metadata($this->result_id) as $i => $field) + { + $retval[$i] = new stdClass(); + $retval[$i]->name = $field['Name']; + $retval[$i]->type = $field['Type']; + $retval[$i]->max_length = $field['Size']; + } + + return $retval; + } + + // -------------------------------------------------------------------- + + /** + * Free the result + * + * @return void + */ + public function free_result() + { + if (is_resource($this->result_id)) + { + sqlsrv_free_stmt($this->result_id); + $this->result_id = FALSE; + } + } + + // -------------------------------------------------------------------- + + /** + * Result - associative array + * + * Returns the result set as an array + * + * @return array + */ + protected function _fetch_assoc() + { + return sqlsrv_fetch_array($this->result_id, SQLSRV_FETCH_ASSOC); + } + + // -------------------------------------------------------------------- + + /** + * Result - object + * + * Returns the result set as an object + * + * @param string $class_name + * @return object + */ + protected function _fetch_object($class_name = 'stdClass') + { + return sqlsrv_fetch_object($this->result_id, $class_name); + } + +} diff --git a/system/database/drivers/sqlsrv/sqlsrv_utility.php b/system/database/drivers/sqlsrv/sqlsrv_utility.php new file mode 100644 index 0000000..e51bc72 --- /dev/null +++ b/system/database/drivers/sqlsrv/sqlsrv_utility.php @@ -0,0 +1,78 @@ +db->display_error('db_unsupported_feature'); + } + +} diff --git a/system/database/index.html b/system/database/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/database/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/fonts/index.html b/system/fonts/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/system/fonts/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/system/fonts/texb.ttf b/system/fonts/texb.ttf new file mode 100644 index 0000000..383c88b Binary files /dev/null and b/system/fonts/texb.ttf differ diff --git a/system/helpers/array_helper.php b/system/helpers/array_helper.php new file mode 100644 index 0000000..0617fde --- /dev/null +++ b/system/helpers/array_helper.php @@ -0,0 +1,116 @@ + '', + 'img_path' => '', + 'img_url' => '', + 'img_width' => '150', + 'img_height' => '30', + 'font_path' => '', + 'expiration' => 7200, + 'word_length' => 8, + 'font_size' => 16, + 'img_id' => '', + 'pool' => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'colors' => array( + 'background' => array(255,255,255), + 'border' => array(153,102,102), + 'text' => array(204,153,153), + 'grid' => array(255,182,182) + ) + ); + + foreach ($defaults as $key => $val) + { + if ( ! is_array($data) && empty($$key)) + { + $$key = $val; + } + else + { + $$key = isset($data[$key]) ? $data[$key] : $val; + } + } + + if ( ! extension_loaded('gd')) + { + log_message('error', 'create_captcha(): GD extension is not loaded.'); + return FALSE; + } + + if ($img_path === '' OR $img_url === '') + { + log_message('error', 'create_captcha(): $img_path and $img_url are required.'); + return FALSE; + } + + if ( ! is_dir($img_path) OR ! is_really_writable($img_path)) + { + log_message('error', "create_captcha(): '{$img_path}' is not a dir, nor is it writable."); + return FALSE; + } + + // ----------------------------------- + // Remove old images + // ----------------------------------- + + $now = microtime(TRUE); + + $current_dir = @opendir($img_path); + while ($filename = @readdir($current_dir)) + { + if (in_array(substr($filename, -4), array('.jpg', '.png')) + && (str_replace(array('.jpg', '.png'), '', $filename) + $expiration) < $now) + { + @unlink($img_path.$filename); + } + } + + @closedir($current_dir); + + // ----------------------------------- + // Do we have a "word" yet? + // ----------------------------------- + + if (empty($word)) + { + $word = ''; + $pool_length = strlen($pool); + $rand_max = $pool_length - 1; + + // PHP7 or a suitable polyfill + if (function_exists('random_int')) + { + try + { + for ($i = 0; $i < $word_length; $i++) + { + $word .= $pool[random_int(0, $rand_max)]; + } + } + catch (Exception $e) + { + // This means fallback to the next possible + // alternative to random_int() + $word = ''; + } + } + } + + if (empty($word)) + { + // Nobody will have a larger character pool than + // 256 characters, but let's handle it just in case ... + // + // No, I do not care that the fallback to mt_rand() can + // handle it; if you trigger this, you're very obviously + // trying to break it. -- Narf + if ($pool_length > 256) + { + return FALSE; + } + + // We'll try using the operating system's PRNG first, + // which we can access through CI_Security::get_random_bytes() + $security = get_instance()->security; + + // To avoid numerous get_random_bytes() calls, we'll + // just try fetching as much bytes as we need at once. + if (($bytes = $security->get_random_bytes($pool_length)) !== FALSE) + { + $byte_index = $word_index = 0; + while ($word_index < $word_length) + { + // Do we have more random data to use? + // It could be exhausted by previous iterations + // ignoring bytes higher than $rand_max. + if ($byte_index === $pool_length) + { + // No failures should be possible if the + // first get_random_bytes() call didn't + // return FALSE, but still ... + for ($i = 0; $i < 5; $i++) + { + if (($bytes = $security->get_random_bytes($pool_length)) === FALSE) + { + continue; + } + + $byte_index = 0; + break; + } + + if ($bytes === FALSE) + { + // Sadly, this means fallback to mt_rand() + $word = ''; + break; + } + } + + list(, $rand_index) = unpack('C', $bytes[$byte_index++]); + if ($rand_index > $rand_max) + { + continue; + } + + $word .= $pool[$rand_index]; + $word_index++; + } + } + } + + if (empty($word)) + { + for ($i = 0; $i < $word_length; $i++) + { + $word .= $pool[mt_rand(0, $rand_max)]; + } + } + elseif ( ! is_string($word)) + { + $word = (string) $word; + } + + // ----------------------------------- + // Determine angle and position + // ----------------------------------- + $length = strlen($word); + $angle = ($length >= 6) ? mt_rand(-($length-6), ($length-6)) : 0; + $x_axis = mt_rand(6, (360/$length)-16); + $y_axis = ($angle >= 0) ? mt_rand($img_height, $img_width) : mt_rand(6, $img_height); + + // Create image + // PHP.net recommends imagecreatetruecolor(), but it isn't always available + $im = function_exists('imagecreatetruecolor') + ? imagecreatetruecolor($img_width, $img_height) + : imagecreate($img_width, $img_height); + + // ----------------------------------- + // Assign colors + // ---------------------------------- + + is_array($colors) OR $colors = $defaults['colors']; + + foreach (array_keys($defaults['colors']) as $key) + { + // Check for a possible missing value + is_array($colors[$key]) OR $colors[$key] = $defaults['colors'][$key]; + $colors[$key] = imagecolorallocate($im, $colors[$key][0], $colors[$key][1], $colors[$key][2]); + } + + // Create the rectangle + ImageFilledRectangle($im, 0, 0, $img_width, $img_height, $colors['background']); + + // ----------------------------------- + // Create the spiral pattern + // ----------------------------------- + $theta = 1; + $thetac = 7; + $radius = 16; + $circles = 20; + $points = 32; + + for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++) + { + $theta += $thetac; + $rad = $radius * ($i / $points); + $x = ($rad * cos($theta)) + $x_axis; + $y = ($rad * sin($theta)) + $y_axis; + $theta += $thetac; + $rad1 = $radius * (($i + 1) / $points); + $x1 = ($rad1 * cos($theta)) + $x_axis; + $y1 = ($rad1 * sin($theta)) + $y_axis; + imageline($im, $x, $y, $x1, $y1, $colors['grid']); + $theta -= $thetac; + } + + // ----------------------------------- + // Write the text + // ----------------------------------- + + $use_font = ($font_path !== '' && file_exists($font_path) && function_exists('imagettftext')); + if ($use_font === FALSE) + { + ($font_size > 5) && $font_size = 5; + $x = mt_rand(0, $img_width / ($length / 3)); + $y = 0; + } + else + { + ($font_size > 30) && $font_size = 30; + $x = mt_rand(0, $img_width / ($length / 1.5)); + $y = $font_size + 2; + } + + for ($i = 0; $i < $length; $i++) + { + if ($use_font === FALSE) + { + $y = mt_rand(0 , $img_height / 2); + imagestring($im, $font_size, $x, $y, $word[$i], $colors['text']); + $x += ($font_size * 2); + } + else + { + $y = mt_rand($img_height / 2, $img_height - 3); + imagettftext($im, $font_size, $angle, $x, $y, $colors['text'], $font_path, $word[$i]); + $x += $font_size; + } + } + + // Create the border + imagerectangle($im, 0, 0, $img_width - 1, $img_height - 1, $colors['border']); + + // ----------------------------------- + // Generate the image + // ----------------------------------- + $img_url = rtrim($img_url, '/').'/'; + + if (function_exists('imagejpeg')) + { + $img_filename = $now.'.jpg'; + imagejpeg($im, $img_path.$img_filename); + } + elseif (function_exists('imagepng')) + { + $img_filename = $now.'.png'; + imagepng($im, $img_path.$img_filename); + } + else + { + return FALSE; + } + + $img = ' '; + ImageDestroy($im); + + return array('word' => $word, 'time' => $now, 'image' => $img, 'filename' => $img_filename); + } +} diff --git a/system/helpers/cookie_helper.php b/system/helpers/cookie_helper.php new file mode 100644 index 0000000..abe492f --- /dev/null +++ b/system/helpers/cookie_helper.php @@ -0,0 +1,114 @@ +input->set_cookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_cookie')) +{ + /** + * Fetch an item from the COOKIE array + * + * @param string + * @param bool + * @return mixed + */ + function get_cookie($index, $xss_clean = NULL) + { + is_bool($xss_clean) OR $xss_clean = (config_item('global_xss_filtering') === TRUE); + $prefix = isset($_COOKIE[$index]) ? '' : config_item('cookie_prefix'); + return get_instance()->input->cookie($prefix.$index, $xss_clean); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('delete_cookie')) +{ + /** + * Delete a COOKIE + * + * @param mixed + * @param string the cookie domain. Usually: .yourdomain.com + * @param string the cookie path + * @param string the cookie prefix + * @return void + */ + function delete_cookie($name, $domain = '', $path = '/', $prefix = '') + { + set_cookie($name, '', '', $domain, $path, $prefix); + } +} diff --git a/system/helpers/date_helper.php b/system/helpers/date_helper.php new file mode 100644 index 0000000..5b2f3e0 --- /dev/null +++ b/system/helpers/date_helper.php @@ -0,0 +1,743 @@ +format('j-n-Y G:i:s'), '%d-%d-%d %d:%d:%d', $day, $month, $year, $hour, $minute, $second); + + return mktime($hour, $minute, $second, $month, $day, $year); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('mdate')) +{ + /** + * Convert MySQL Style Datecodes + * + * This function is identical to PHPs date() function, + * except that it allows date codes to be formatted using + * the MySQL style, where each code letter is preceded + * with a percent sign: %Y %m %d etc... + * + * The benefit of doing dates this way is that you don't + * have to worry about escaping your text letters that + * match the date codes. + * + * @param string + * @param int + * @return int + */ + function mdate($datestr = '', $time = '') + { + if ($datestr === '') + { + return ''; + } + elseif (empty($time)) + { + $time = now(); + } + + $datestr = str_replace( + '%\\', + '', + preg_replace('/([a-z]+?){1}/i', '\\\\\\1', $datestr) + ); + + return date($datestr, $time); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('standard_date')) +{ + /** + * Standard Date + * + * Returns a date formatted according to the submitted standard. + * + * As of PHP 5.2, the DateTime extension provides constants that + * serve for the exact same purpose and are used with date(). + * + * @todo Remove in version 3.1+. + * @deprecated 3.0.0 Use PHP's native date() instead. + * @link https://www.php.net/manual/en/class.datetime.php#datetime.constants.types + * + * @example date(DATE_RFC822, now()); // default + * @example date(DATE_W3C, $time); // a different format and time + * + * @param string $fmt = 'DATE_RFC822' the chosen format + * @param int $time = NULL Unix timestamp + * @return string + */ + function standard_date($fmt = 'DATE_RFC822', $time = NULL) + { + if (empty($time)) + { + $time = now(); + } + + // Procedural style pre-defined constants from the DateTime extension + if (strpos($fmt, 'DATE_') !== 0 OR defined($fmt) === FALSE) + { + return FALSE; + } + + return date(constant($fmt), $time); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('timespan')) +{ + /** + * Timespan + * + * Returns a span of seconds in this format: + * 10 days 14 hours 36 minutes 47 seconds + * + * @param int a number of seconds + * @param int Unix timestamp + * @param int a number of display units + * @return string + */ + function timespan($seconds = 1, $time = '', $units = 7) + { + $CI =& get_instance(); + $CI->lang->load('date'); + + is_numeric($seconds) OR $seconds = 1; + is_numeric($time) OR $time = time(); + is_numeric($units) OR $units = 7; + + $seconds = ($time <= $seconds) ? 1 : $time - $seconds; + + $str = array(); + $years = floor($seconds / 31557600); + + if ($years > 0) + { + $str[] = $years.' '.$CI->lang->line($years > 1 ? 'date_years' : 'date_year'); + } + + $seconds -= $years * 31557600; + $months = floor($seconds / 2629743); + + if (count($str) < $units && ($years > 0 OR $months > 0)) + { + if ($months > 0) + { + $str[] = $months.' '.$CI->lang->line($months > 1 ? 'date_months' : 'date_month'); + } + + $seconds -= $months * 2629743; + } + + $weeks = floor($seconds / 604800); + + if (count($str) < $units && ($years > 0 OR $months > 0 OR $weeks > 0)) + { + if ($weeks > 0) + { + $str[] = $weeks.' '.$CI->lang->line($weeks > 1 ? 'date_weeks' : 'date_week'); + } + + $seconds -= $weeks * 604800; + } + + $days = floor($seconds / 86400); + + if (count($str) < $units && ($months > 0 OR $weeks > 0 OR $days > 0)) + { + if ($days > 0) + { + $str[] = $days.' '.$CI->lang->line($days > 1 ? 'date_days' : 'date_day'); + } + + $seconds -= $days * 86400; + } + + $hours = floor($seconds / 3600); + + if (count($str) < $units && ($days > 0 OR $hours > 0)) + { + if ($hours > 0) + { + $str[] = $hours.' '.$CI->lang->line($hours > 1 ? 'date_hours' : 'date_hour'); + } + + $seconds -= $hours * 3600; + } + + $minutes = floor($seconds / 60); + + if (count($str) < $units && ($days > 0 OR $hours > 0 OR $minutes > 0)) + { + if ($minutes > 0) + { + $str[] = $minutes.' '.$CI->lang->line($minutes > 1 ? 'date_minutes' : 'date_minute'); + } + + $seconds -= $minutes * 60; + } + + if (count($str) === 0) + { + $str[] = $seconds.' '.$CI->lang->line($seconds > 1 ? 'date_seconds' : 'date_second'); + } + + return implode(', ', $str); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('days_in_month')) +{ + /** + * Number of days in a month + * + * Takes a month/year as input and returns the number of days + * for the given month/year. Takes leap years into consideration. + * + * @param int a numeric month + * @param int a numeric year + * @return int + */ + function days_in_month($month = 0, $year = '') + { + if ($month < 1 OR $month > 12) + { + return 0; + } + elseif ( ! is_numeric($year) OR strlen($year) !== 4) + { + $year = date('Y'); + } + + if (defined('CAL_GREGORIAN')) + { + return cal_days_in_month(CAL_GREGORIAN, $month, $year); + } + + if ($year >= 1970) + { + return (int) date('t', mktime(12, 0, 0, $month, 1, $year)); + } + + if ($month == 2) + { + if ($year % 400 === 0 OR ($year % 4 === 0 && $year % 100 !== 0)) + { + return 29; + } + } + + $days_in_month = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + return $days_in_month[$month - 1]; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('local_to_gmt')) +{ + /** + * Converts a local Unix timestamp to GMT + * + * @param int Unix timestamp + * @return int + */ + function local_to_gmt($time = '') + { + if ($time === '') + { + $time = time(); + } + + return mktime( + gmdate('G', $time), + gmdate('i', $time), + gmdate('s', $time), + gmdate('n', $time), + gmdate('j', $time), + gmdate('Y', $time) + ); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('gmt_to_local')) +{ + /** + * Converts GMT time to a localized value + * + * Takes a Unix timestamp (in GMT) as input, and returns + * at the local value based on the timezone and DST setting + * submitted + * + * @param int Unix timestamp + * @param string timezone + * @param bool whether DST is active + * @return int + */ + function gmt_to_local($time = '', $timezone = 'UTC', $dst = FALSE) + { + if ($time === '') + { + return now(); + } + + $time += timezones($timezone) * 3600; + + return ($dst === TRUE) ? $time + 3600 : $time; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('mysql_to_unix')) +{ + /** + * Converts a MySQL Timestamp to Unix + * + * @param int MySQL timestamp YYYY-MM-DD HH:MM:SS + * @return int Unix timstamp + */ + function mysql_to_unix($time = '') + { + // We'll remove certain characters for backward compatibility + // since the formatting changed with MySQL 4.1 + // YYYY-MM-DD HH:MM:SS + + $time = str_replace(array('-', ':', ' '), '', $time); + + // YYYYMMDDHHMMSS + return mktime( + substr($time, 8, 2), + substr($time, 10, 2), + substr($time, 12, 2), + substr($time, 4, 2), + substr($time, 6, 2), + substr($time, 0, 4) + ); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('unix_to_human')) +{ + /** + * Unix to "Human" + * + * Formats Unix timestamp to the following prototype: 2006-08-21 11:35 PM + * + * @param int Unix timestamp + * @param bool whether to show seconds + * @param string format: us or euro + * @return string + */ + function unix_to_human($time = '', $seconds = FALSE, $fmt = 'us') + { + $r = date('Y', $time).'-'.date('m', $time).'-'.date('d', $time).' '; + + if ($fmt === 'us') + { + $r .= date('h', $time).':'.date('i', $time); + } + else + { + $r .= date('H', $time).':'.date('i', $time); + } + + if ($seconds) + { + $r .= ':'.date('s', $time); + } + + if ($fmt === 'us') + { + return $r.' '.date('A', $time); + } + + return $r; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('human_to_unix')) +{ + /** + * Convert "human" date to GMT + * + * Reverses the above process + * + * @param string format: us or euro + * @return int + */ + function human_to_unix($datestr = '') + { + if ($datestr === '') + { + return FALSE; + } + + $datestr = preg_replace('/\040+/', ' ', trim($datestr)); + + if ( ! preg_match('/^(\d{2}|\d{4})\-[0-9]{1,2}\-[0-9]{1,2}\s[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?(?:\s[AP]M)?$/i', $datestr)) + { + return FALSE; + } + + sscanf($datestr, '%d-%d-%d %s %s', $year, $month, $day, $time, $ampm); + sscanf($time, '%d:%d:%d', $hour, $min, $sec); + isset($sec) OR $sec = 0; + + if (isset($ampm)) + { + $ampm = strtolower($ampm); + + if ($ampm[0] === 'p' && $hour < 12) + { + $hour += 12; + } + elseif ($ampm[0] === 'a' && $hour === 12) + { + $hour = 0; + } + } + + return mktime($hour, $min, $sec, $month, $day, $year); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('nice_date')) +{ + /** + * Turns many "reasonably-date-like" strings into something + * that is actually useful. This only works for dates after unix epoch. + * + * @deprecated 3.1.3 Use DateTime::createFromFormat($input_format, $input)->format($output_format); + * @param string The terribly formatted date-like string + * @param string Date format to return (same as php date function) + * @return string + */ + function nice_date($bad_date = '', $format = FALSE) + { + if (empty($bad_date)) + { + return 'Unknown'; + } + elseif (empty($format)) + { + $format = 'U'; + } + + // Date like: YYYYMM + if (preg_match('/^\d{6}$/i', $bad_date)) + { + if (in_array(substr($bad_date, 0, 2), array('19', '20'))) + { + $year = substr($bad_date, 0, 4); + $month = substr($bad_date, 4, 2); + } + else + { + $month = substr($bad_date, 0, 2); + $year = substr($bad_date, 2, 4); + } + + return date($format, strtotime($year.'-'.$month.'-01')); + } + + // Date Like: YYYYMMDD + if (preg_match('/^\d{8}$/i', $bad_date, $matches)) + { + return DateTime::createFromFormat('Ymd', $bad_date)->format($format); + } + + // Date Like: MM-DD-YYYY __or__ M-D-YYYY (or anything in between) + if (preg_match('/^(\d{1,2})-(\d{1,2})-(\d{4})$/i', $bad_date, $matches)) + { + return date($format, strtotime($matches[3].'-'.$matches[1].'-'.$matches[2])); + } + + // Any other kind of string, when converted into UNIX time, + // produces "0 seconds after epoc..." is probably bad... + // return "Invalid Date". + if (date('U', strtotime($bad_date)) === '0') + { + return 'Invalid Date'; + } + + // It's probably a valid-ish date format already + return date($format, strtotime($bad_date)); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('timezone_menu')) +{ + /** + * Timezone Menu + * + * Generates a drop-down menu of timezones. + * + * @param string timezone + * @param string classname + * @param string menu name + * @param mixed attributes + * @return string + */ + function timezone_menu($default = 'UTC', $class = '', $name = 'timezones', $attributes = '') + { + $CI =& get_instance(); + $CI->lang->load('date'); + + $default = ($default === 'GMT') ? 'UTC' : $default; + + $menu = ''; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('timezones')) +{ + /** + * Timezones + * + * Returns an array of timezones. This is a helper function + * for various other ones in this library + * + * @param string timezone + * @return string + */ + function timezones($tz = '') + { + // Note: Don't change the order of these even though + // some items appear to be in the wrong order + + $zones = array( + 'UM12' => -12, + 'UM11' => -11, + 'UM10' => -10, + 'UM95' => -9.5, + 'UM9' => -9, + 'UM8' => -8, + 'UM7' => -7, + 'UM6' => -6, + 'UM5' => -5, + 'UM45' => -4.5, + 'UM4' => -4, + 'UM35' => -3.5, + 'UM3' => -3, + 'UM2' => -2, + 'UM1' => -1, + 'UTC' => 0, + 'UP1' => +1, + 'UP2' => +2, + 'UP3' => +3, + 'UP35' => +3.5, + 'UP4' => +4, + 'UP45' => +4.5, + 'UP5' => +5, + 'UP55' => +5.5, + 'UP575' => +5.75, + 'UP6' => +6, + 'UP65' => +6.5, + 'UP7' => +7, + 'UP8' => +8, + 'UP875' => +8.75, + 'UP9' => +9, + 'UP95' => +9.5, + 'UP10' => +10, + 'UP105' => +10.5, + 'UP11' => +11, + 'UP115' => +11.5, + 'UP12' => +12, + 'UP1275' => +12.75, + 'UP13' => +13, + 'UP14' => +14 + ); + + if ($tz === '') + { + return $zones; + } + + return isset($zones[$tz]) ? $zones[$tz] : 0; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('date_range')) +{ + /** + * Date range + * + * Returns a list of dates within a specified period. + * + * @param int unix_start UNIX timestamp of period start date + * @param int unix_end|days UNIX timestamp of period end date + * or interval in days. + * @param mixed is_unix Specifies whether the second parameter + * is a UNIX timestamp or a day interval + * - TRUE or 'unix' for a timestamp + * - FALSE or 'days' for an interval + * @param string date_format Output date format, same as in date() + * @return array + */ + function date_range($unix_start = '', $mixed = '', $is_unix = TRUE, $format = 'Y-m-d') + { + if ($unix_start == '' OR $mixed == '' OR $format == '') + { + return FALSE; + } + + $is_unix = ! ( ! $is_unix OR $is_unix === 'days'); + + // Validate input and try strtotime() on invalid timestamps/intervals, just in case + if ( ( ! ctype_digit((string) $unix_start) && ($unix_start = @strtotime($unix_start)) === FALSE) + OR ( ! ctype_digit((string) $mixed) && ($is_unix === FALSE OR ($mixed = @strtotime($mixed)) === FALSE)) + OR ($is_unix === TRUE && $mixed < $unix_start)) + { + return FALSE; + } + + if ($is_unix && ($unix_start == $mixed OR date($format, $unix_start) === date($format, $mixed))) + { + return array(date($format, $unix_start)); + } + + $range = array(); + + $from = new DateTime(); + $from->setTimestamp($unix_start); + + if ($is_unix) + { + $arg = new DateTime(); + $arg->setTimestamp($mixed); + } + else + { + $arg = (int) $mixed; + } + + $period = new DatePeriod($from, new DateInterval('P1D'), $arg); + foreach ($period as $date) + { + $range[] = $date->format($format); + } + + /* If a period end date was passed to the DatePeriod constructor, it might not + * be in our results. Not sure if this is a bug or it's just possible because + * the end date might actually be less than 24 hours away from the previously + * generated DateTime object, but either way - we have to append it manually. + */ + if ( ! is_int($arg) && $range[count($range) - 1] !== $arg->format($format)) + { + $range[] = $arg->format($format); + } + + return $range; + } +} diff --git a/system/helpers/directory_helper.php b/system/helpers/directory_helper.php new file mode 100644 index 0000000..d747a96 --- /dev/null +++ b/system/helpers/directory_helper.php @@ -0,0 +1,102 @@ + 0) && is_dir($source_dir.$file)) + { + $filedata[$file] = directory_map($source_dir.$file, $new_depth, $hidden); + } + else + { + $filedata[] = $file; + } + } + + closedir($fp); + return $filedata; + } + + return FALSE; + } +} diff --git a/system/helpers/download_helper.php b/system/helpers/download_helper.php new file mode 100644 index 0000000..9b361c4 --- /dev/null +++ b/system/helpers/download_helper.php @@ -0,0 +1,159 @@ + 0) + ? @rmdir($path) + : TRUE; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('get_filenames')) +{ + /** + * Get Filenames + * + * Reads the specified directory and builds an array containing the filenames. + * Any sub-folders contained within the specified path are read as well. + * + * @param string path to source + * @param bool whether to include the path as part of the filename + * @param bool internal variable to determine recursion status - do not use in calls + * @return array + */ + function get_filenames($source_dir, $include_path = FALSE, $_recursion = FALSE) + { + static $_filedata = array(); + + if ($fp = @opendir($source_dir)) + { + // reset the array and make sure $source_dir has a trailing slash on the initial call + if ($_recursion === FALSE) + { + $_filedata = array(); + $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + } + + while (FALSE !== ($file = readdir($fp))) + { + if (is_dir($source_dir.$file) && $file[0] !== '.') + { + get_filenames($source_dir.$file.DIRECTORY_SEPARATOR, $include_path, TRUE); + } + elseif ($file[0] !== '.') + { + $_filedata[] = ($include_path === TRUE) ? $source_dir.$file : $file; + } + } + + closedir($fp); + return $_filedata; + } + + return FALSE; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_dir_file_info')) +{ + /** + * Get Directory File Information + * + * Reads the specified directory and builds an array containing the filenames, + * filesize, dates, and permissions + * + * Any sub-folders contained within the specified path are read as well. + * + * @param string path to source + * @param bool Look only at the top level directory specified? + * @param bool internal variable to determine recursion status - do not use in calls + * @return array + */ + function get_dir_file_info($source_dir, $top_level_only = TRUE, $_recursion = FALSE) + { + static $_filedata = array(); + $relative_path = $source_dir; + + if ($fp = @opendir($source_dir)) + { + // reset the array and make sure $source_dir has a trailing slash on the initial call + if ($_recursion === FALSE) + { + $_filedata = array(); + $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + } + + // Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast + while (FALSE !== ($file = readdir($fp))) + { + if (is_dir($source_dir.$file) && $file[0] !== '.' && $top_level_only === FALSE) + { + get_dir_file_info($source_dir.$file.DIRECTORY_SEPARATOR, $top_level_only, TRUE); + } + elseif ($file[0] !== '.') + { + $_filedata[$file] = get_file_info($source_dir.$file); + $_filedata[$file]['relative_path'] = $relative_path; + } + } + + closedir($fp); + return $_filedata; + } + + return FALSE; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_file_info')) +{ + /** + * Get File Info + * + * Given a file and path, returns the name, path, size, date modified + * Second parameter allows you to explicitly declare what information you want returned + * Options are: name, server_path, size, date, readable, writable, executable, fileperms + * Returns FALSE if the file cannot be found. + * + * @param string path to file + * @param mixed array or comma separated string of information returned + * @return array + */ + function get_file_info($file, $returned_values = array('name', 'server_path', 'size', 'date')) + { + if ( ! file_exists($file)) + { + return FALSE; + } + + if (is_string($returned_values)) + { + $returned_values = explode(',', $returned_values); + } + + foreach ($returned_values as $key) + { + switch ($key) + { + case 'name': + $fileinfo['name'] = basename($file); + break; + case 'server_path': + $fileinfo['server_path'] = $file; + break; + case 'size': + $fileinfo['size'] = filesize($file); + break; + case 'date': + $fileinfo['date'] = filemtime($file); + break; + case 'readable': + $fileinfo['readable'] = is_readable($file); + break; + case 'writable': + $fileinfo['writable'] = is_really_writable($file); + break; + case 'executable': + $fileinfo['executable'] = is_executable($file); + break; + case 'fileperms': + $fileinfo['fileperms'] = fileperms($file); + break; + } + } + + return $fileinfo; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('get_mime_by_extension')) +{ + /** + * Get Mime by Extension + * + * Translates a file extension into a mime type based on config/mimes.php. + * Returns FALSE if it can't determine the type, or open the mime config file + * + * Note: this is NOT an accurate way of determining file mime types, and is here strictly as a convenience + * It should NOT be trusted, and should certainly NOT be used for security + * + * @param string $filename File name + * @return string + */ + function get_mime_by_extension($filename) + { + static $mimes; + + if ( ! is_array($mimes)) + { + $mimes = get_mimes(); + + if (empty($mimes)) + { + return FALSE; + } + } + + $extension = strtolower(substr(strrchr($filename, '.'), 1)); + + if (isset($mimes[$extension])) + { + return is_array($mimes[$extension]) + ? current($mimes[$extension]) // Multiple mime types, just give the first one + : $mimes[$extension]; + } + + return FALSE; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('symbolic_permissions')) +{ + /** + * Symbolic Permissions + * + * Takes a numeric value representing a file's permissions and returns + * standard symbolic notation representing that value + * + * @param int $perms Permissions + * @return string + */ + function symbolic_permissions($perms) + { + if (($perms & 0xC000) === 0xC000) + { + $symbolic = 's'; // Socket + } + elseif (($perms & 0xA000) === 0xA000) + { + $symbolic = 'l'; // Symbolic Link + } + elseif (($perms & 0x8000) === 0x8000) + { + $symbolic = '-'; // Regular + } + elseif (($perms & 0x6000) === 0x6000) + { + $symbolic = 'b'; // Block special + } + elseif (($perms & 0x4000) === 0x4000) + { + $symbolic = 'd'; // Directory + } + elseif (($perms & 0x2000) === 0x2000) + { + $symbolic = 'c'; // Character special + } + elseif (($perms & 0x1000) === 0x1000) + { + $symbolic = 'p'; // FIFO pipe + } + else + { + $symbolic = 'u'; // Unknown + } + + // Owner + $symbolic .= (($perms & 0x0100) ? 'r' : '-') + .(($perms & 0x0080) ? 'w' : '-') + .(($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-')); + + // Group + $symbolic .= (($perms & 0x0020) ? 'r' : '-') + .(($perms & 0x0010) ? 'w' : '-') + .(($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-')); + + // World + $symbolic .= (($perms & 0x0004) ? 'r' : '-') + .(($perms & 0x0002) ? 'w' : '-') + .(($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-')); + + return $symbolic; + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('octal_permissions')) +{ + /** + * Octal Permissions + * + * Takes a numeric value representing a file's permissions and returns + * a three character string representing the file's octal permissions + * + * @param int $perms Permissions + * @return string + */ + function octal_permissions($perms) + { + return substr(sprintf('%o', $perms), -3); + } +} diff --git a/system/helpers/form_helper.php b/system/helpers/form_helper.php new file mode 100644 index 0000000..ba74ff5 --- /dev/null +++ b/system/helpers/form_helper.php @@ -0,0 +1,1056 @@ +config->site_url($CI->uri->uri_string()); + } + // If an action is not a full URL then turn it into one + elseif (strpos($action, '://') === FALSE) + { + $action = $CI->config->site_url($action); + } + + $attributes = _attributes_to_string($attributes); + + if (stripos($attributes, 'method=') === FALSE) + { + $attributes .= ' method="post"'; + } + + if (stripos($attributes, 'accept-charset=') === FALSE) + { + $attributes .= ' accept-charset="'.strtolower(config_item('charset')).'"'; + } + + $form = '
\n"; + + if (is_array($hidden)) + { + foreach ($hidden as $name => $value) + { + $form .= ''."\n"; + } + } + + // Add CSRF field if enabled, but leave it out for GET requests and requests to external websites + if ($CI->config->item('csrf_protection') === TRUE && strpos($action, $CI->config->base_url()) !== FALSE && ! stripos($form, 'method="get"')) + { + // Prepend/append random-length "white noise" around the CSRF + // token input, as a form of protection against BREACH attacks + if (FALSE !== ($noise = $CI->security->get_random_bytes(1))) + { + list(, $noise) = unpack('c', $noise); + } + else + { + $noise = mt_rand(-128, 127); + } + + // Prepend if $noise has a negative value, append if positive, do nothing for zero + $prepend = $append = ''; + if ($noise < 0) + { + $prepend = str_repeat(" ", abs($noise)); + } + elseif ($noise > 0) + { + $append = str_repeat(" ", $noise); + } + + $form .= sprintf( + '%s%s%s', + $prepend, + $CI->security->get_csrf_token_name(), + $CI->security->get_csrf_hash(), + $append, + "\n" + ); + } + + return $form; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_open_multipart')) +{ + /** + * Form Declaration - Multipart type + * + * Creates the opening portion of the form, but with "multipart/form-data". + * + * @param string the URI segments of the form destination + * @param array a key/value pair of attributes + * @param array a key/value pair hidden data + * @return string + */ + function form_open_multipart($action = '', $attributes = array(), $hidden = array()) + { + if (is_string($attributes)) + { + $attributes .= ' enctype="multipart/form-data"'; + } + else + { + $attributes['enctype'] = 'multipart/form-data'; + } + + return form_open($action, $attributes, $hidden); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_hidden')) +{ + /** + * Hidden Input Field + * + * Generates hidden fields. You can pass a simple key/value string or + * an associative array with multiple values. + * + * @param mixed $name Field name + * @param string $value Field value + * @param bool $recursing + * @return string + */ + function form_hidden($name, $value = '', $recursing = FALSE) + { + static $form; + + if ($recursing === FALSE) + { + $form = "\n"; + } + + if (is_array($name)) + { + foreach ($name as $key => $val) + { + form_hidden($key, $val, TRUE); + } + + return $form; + } + + if ( ! is_array($value)) + { + $form .= '\n"; + } + else + { + foreach ($value as $k => $v) + { + $k = is_int($k) ? '' : $k; + form_hidden($name.'['.$k.']', $v, TRUE); + } + } + + return $form; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_input')) +{ + /** + * Text Input Field + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_input($data = '', $value = '', $extra = '') + { + $defaults = array( + 'type' => 'text', + 'name' => is_array($data) ? '' : $data, + 'value' => $value + ); + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_password')) +{ + /** + * Password Field + * + * Identical to the input function but adds the "password" type + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_password($data = '', $value = '', $extra = '') + { + is_array($data) OR $data = array('name' => $data); + $data['type'] = 'password'; + return form_input($data, $value, $extra); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_upload')) +{ + /** + * Upload Field + * + * Identical to the input function but adds the "file" type + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_upload($data = '', $value = '', $extra = '') + { + $defaults = array('type' => 'file', 'name' => ''); + is_array($data) OR $data = array('name' => $data); + $data['type'] = 'file'; + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_textarea')) +{ + /** + * Textarea field + * + * @param mixed $data + * @param string $value + * @param mixed $extra + * @return string + */ + function form_textarea($data = '', $value = '', $extra = '') + { + $defaults = array( + 'name' => is_array($data) ? '' : $data, + 'cols' => '40', + 'rows' => '10' + ); + + if ( ! is_array($data) OR ! isset($data['value'])) + { + $val = $value; + } + else + { + $val = $data['value']; + unset($data['value']); // textareas don't use the value attribute + } + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_multiselect')) +{ + /** + * Multi-select menu + * + * @param string + * @param array + * @param mixed + * @param mixed + * @return string + */ + function form_multiselect($name = '', $options = array(), $selected = array(), $extra = '') + { + $extra = _attributes_to_string($extra); + if (stripos($extra, 'multiple') === FALSE) + { + $extra .= ' multiple="multiple"'; + } + + return form_dropdown($name, $options, $selected, $extra); + } +} + +// -------------------------------------------------------------------- + +if ( ! function_exists('form_dropdown')) +{ + /** + * Drop-down Menu + * + * @param mixed $data + * @param mixed $options + * @param mixed $selected + * @param mixed $extra + * @return string + */ + function form_dropdown($data = '', $options = array(), $selected = array(), $extra = '') + { + $defaults = array(); + + if (is_array($data)) + { + if (isset($data['selected'])) + { + $selected = $data['selected']; + unset($data['selected']); // select tags don't have a selected attribute + } + + if (isset($data['options'])) + { + $options = $data['options']; + unset($data['options']); // select tags don't use an options attribute + } + } + else + { + $defaults = array('name' => $data); + } + + is_array($selected) OR $selected = array($selected); + is_array($options) OR $options = array($options); + + // If no selected state was submitted we will attempt to set it automatically + if (empty($selected)) + { + if (is_array($data)) + { + if (isset($data['name'], $_POST[$data['name']])) + { + $selected = array($_POST[$data['name']]); + } + } + elseif (isset($_POST[$data])) + { + $selected = array($_POST[$data]); + } + } + + $extra = _attributes_to_string($extra); + + $multiple = (count($selected) > 1 && stripos($extra, 'multiple') === FALSE) ? ' multiple="multiple"' : ''; + + $form = '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_checkbox')) +{ + /** + * Checkbox Field + * + * @param mixed + * @param string + * @param bool + * @param mixed + * @return string + */ + function form_checkbox($data = '', $value = '', $checked = FALSE, $extra = '') + { + $defaults = array('type' => 'checkbox', 'name' => ( ! is_array($data) ? $data : ''), 'value' => $value); + + if (is_array($data) && array_key_exists('checked', $data)) + { + $checked = $data['checked']; + + if ($checked == FALSE) + { + unset($data['checked']); + } + else + { + $data['checked'] = 'checked'; + } + } + + if ($checked == TRUE) + { + $defaults['checked'] = 'checked'; + } + else + { + unset($defaults['checked']); + } + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_radio')) +{ + /** + * Radio Button + * + * @param mixed + * @param string + * @param bool + * @param mixed + * @return string + */ + function form_radio($data = '', $value = '', $checked = FALSE, $extra = '') + { + is_array($data) OR $data = array('name' => $data); + $data['type'] = 'radio'; + + return form_checkbox($data, $value, $checked, $extra); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_submit')) +{ + /** + * Submit Button + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_submit($data = '', $value = '', $extra = '') + { + $defaults = array( + 'type' => 'submit', + 'name' => is_array($data) ? '' : $data, + 'value' => $value + ); + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_reset')) +{ + /** + * Reset Button + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_reset($data = '', $value = '', $extra = '') + { + $defaults = array( + 'type' => 'reset', + 'name' => is_array($data) ? '' : $data, + 'value' => $value + ); + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_button')) +{ + /** + * Form Button + * + * @param mixed + * @param string + * @param mixed + * @return string + */ + function form_button($data = '', $content = '', $extra = '') + { + $defaults = array( + 'name' => is_array($data) ? '' : $data, + 'type' => 'button' + ); + + if (is_array($data) && isset($data['content'])) + { + $content = $data['content']; + unset($data['content']); // content is not an attribute + } + + return '\n"; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_label')) +{ + /** + * Form Label Tag + * + * @param string The text to appear onscreen + * @param string The id the label applies to + * @param mixed Additional attributes + * @return string + */ + function form_label($label_text = '', $id = '', $attributes = array()) + { + + $label = ''.$label_text.''; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_fieldset')) +{ + /** + * Fieldset Tag + * + * Used to produce
text. To close fieldset + * use form_fieldset_close() + * + * @param string The legend text + * @param array Additional attributes + * @return string + */ + function form_fieldset($legend_text = '', $attributes = array()) + { + $fieldset = '\n"; + if ($legend_text !== '') + { + return $fieldset.''.$legend_text."\n"; + } + + return $fieldset; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_fieldset_close')) +{ + /** + * Fieldset Close Tag + * + * @param string + * @return string + */ + function form_fieldset_close($extra = '') + { + return '
'.$extra; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_close')) +{ + /** + * Form Close Tag + * + * @param string + * @return string + */ + function form_close($extra = '') + { + return ''.$extra; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('form_prep')) +{ + /** + * Form Prep + * + * Formats text so that it can be safely placed in a form field in the event it has HTML tags. + * + * @deprecated 3.0.0 An alias for html_escape() + * @param string|string[] $str Value to escape + * @return string|string[] Escaped values + */ + function form_prep($str) + { + return html_escape($str, TRUE); + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('set_value')) +{ + /** + * Form Value + * + * Grabs a value from the POST array for the specified field so you can + * re-populate an input field or textarea. If Form Validation + * is active it retrieves the info from the validation class + * + * @param string $field Field name + * @param string $default Default value + * @param bool $html_escape Whether to escape HTML special characters or not + * @return string + */ + function set_value($field, $default = '', $html_escape = TRUE) + { + $CI =& get_instance(); + + $value = (isset($CI->form_validation) && is_object($CI->form_validation) && $CI->form_validation->has_rule($field)) + ? $CI->form_validation->set_value($field, $default) + : $CI->input->post($field, FALSE); + + isset($value) OR $value = $default; + return ($html_escape) ? html_escape($value) : $value; + } +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('set_select')) +{ + /** + * Set Select + * + * Let's you set the selected value of a + + "; + + foreach ($this->data as $g => $glyph) { + if ($n-- <= 0) { + break; + } + + $glyph->parseData(); + + $shape = array( + "SVGContours" => $glyph->getSVGContours(), + "xMin" => $glyph->xMin, + "yMin" => $glyph->yMin, + "xMax" => $glyph->xMax, + "yMax" => $glyph->yMax, + ); + $shape_json = json_encode($shape); + + $type = ($glyph instanceof OutlineSimple ? "simple" : "composite"); + $char = isset($glyphIndexArray[$g]) ? $glyphIndexArray[$g] : 0; + $name = isset($names[$g]) ? $names[$g] : sprintf("uni%04x", $char); + $char = $char ? "&#{$glyphIndexArray[$g]};" : ""; + + if ($char === "" && empty($shape["SVGContours"])) { + $n++; + continue; + } + + $s .= "
+ "; + } + + return $s; + } + + + protected function _encode() { + $font = $this->getFont(); + $subset = $font->getSubset(); + $data = $this->data; + + $loca = array(); + + $length = 0; + foreach ($subset as $gid) { + $loca[] = $length; + + $bytes = $data[$gid]->encode(); + + $pad = 0; + $mod = $bytes % 4; + if ($mod != 0) { + $pad = 4 - $mod; + $font->write(str_pad("", $pad, "\0"), $pad); + } + $length += $bytes + $pad; + } + + $loca[] = $length; // dummy loca + $font->getTableObject("loca")->data = $loca; + + return $length; + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/head.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/head.php new file mode 100644 index 0000000..ce5a3a4 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/head.php @@ -0,0 +1,50 @@ + self::Fixed, + "fontRevision" => self::Fixed, + "checkSumAdjustment" => self::uint32, + "magicNumber" => self::uint32, + "flags" => self::uint16, + "unitsPerEm" => self::uint16, + "created" => self::longDateTime, + "modified" => self::longDateTime, + "xMin" => self::FWord, + "yMin" => self::FWord, + "xMax" => self::FWord, + "yMax" => self::FWord, + "macStyle" => self::uint16, + "lowestRecPPEM" => self::uint16, + "fontDirectionHint" => self::int16, + "indexToLocFormat" => self::int16, + "glyphDataFormat" => self::int16, + ); + + protected function _parse() { + parent::_parse(); + + if ($this->data["magicNumber"] != 0x5F0F3CF5) { + throw new Exception("Incorrect magic number (" . dechex($this->data["magicNumber"]) . ")"); + } + } + + function _encode() { + $this->data["checkSumAdjustment"] = 0; + return parent::_encode(); + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/hhea.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/hhea.php new file mode 100644 index 0000000..becb9a4 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/hhea.php @@ -0,0 +1,43 @@ + self::Fixed, + "ascent" => self::FWord, + "descent" => self::FWord, + "lineGap" => self::FWord, + "advanceWidthMax" => self::uFWord, + "minLeftSideBearing" => self::FWord, + "minRightSideBearing" => self::FWord, + "xMaxExtent" => self::FWord, + "caretSlopeRise" => self::int16, + "caretSlopeRun" => self::int16, + "caretOffset" => self::FWord, + self::int16, + self::int16, + self::int16, + self::int16, + "metricDataFormat" => self::int16, + "numOfLongHorMetrics" => self::uint16, + ); + + function _encode() { + $font = $this->getFont(); + $this->data["numOfLongHorMetrics"] = count($font->getSubset()); + + return parent::_encode(); + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/hmtx.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/hmtx.php new file mode 100644 index 0000000..ccd37d7 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/hmtx.php @@ -0,0 +1,64 @@ +getFont(); + $offset = $font->pos(); + + $numOfLongHorMetrics = $font->getData("hhea", "numOfLongHorMetrics"); + $numGlyphs = $font->getData("maxp", "numGlyphs"); + + $font->seek($offset); + + $data = array(); + $metrics = $font->readUInt16Many($numOfLongHorMetrics * 2); + for ($gid = 0, $mid = 0; $gid < $numOfLongHorMetrics; $gid++) { + $advanceWidth = isset($metrics[$mid]) ? $metrics[$mid] : 0; + $mid += 1; + $leftSideBearing = isset($metrics[$mid]) ? $metrics[$mid] : 0; + $mid += 1; + $data[$gid] = array($advanceWidth, $leftSideBearing); + } + + if ($numOfLongHorMetrics < $numGlyphs) { + $lastWidth = end($data)[0]; + $numLeft = $numGlyphs - $numOfLongHorMetrics; + $metrics = $font->readUInt16Many($numLeft); + for($i = 0; $i < $numLeft; $i++) { + $gid = $numOfLongHorMetrics + $i; + $leftSideBearing = isset($metrics[$i]) ? $metrics[$i] : 0; + $data[$gid] = array($lastWidth, $leftSideBearing); + } + } + + $this->data = $data; + } + + protected function _encode() { + $font = $this->getFont(); + $subset = $font->getSubset(); + $data = $this->data; + + $length = 0; + + foreach ($subset as $gid) { + $length += $font->writeUInt16($data[$gid][0]); + $length += $font->writeUInt16($data[$gid][1]); + } + + return $length; + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/kern.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/kern.php new file mode 100644 index 0000000..75f2c6f --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/kern.php @@ -0,0 +1,79 @@ +getFont(); + + $data = $font->unpack(array( + "version" => self::uint16, + "nTables" => self::uint16, + + // only the first subtable will be parsed + "subtableVersion" => self::uint16, + "length" => self::uint16, + "coverage" => self::uint16, + )); + + $data["format"] = ($data["coverage"] >> 8); + + $subtable = array(); + + switch ($data["format"]) { + case 0: + $subtable = $font->unpack(array( + "nPairs" => self::uint16, + "searchRange" => self::uint16, + "entrySelector" => self::uint16, + "rangeShift" => self::uint16, + )); + + $pairs = array(); + $tree = array(); + + $values = $font->readUInt16Many($subtable["nPairs"] * 3); + for ($i = 0, $idx = 0; $i < $subtable["nPairs"]; $i++) { + $left = $values[$idx++]; + $right = $values[$idx++]; + $value = $values[$idx++]; + + if ($value >= 0x8000) { + $value -= 0x10000; + } + + $pairs[] = array( + "left" => $left, + "right" => $right, + "value" => $value, + ); + + $tree[$left][$right] = $value; + } + + //$subtable["pairs"] = $pairs; + $subtable["tree"] = $tree; + break; + + case 1: + case 2: + case 3: + break; + } + + $data["subtable"] = $subtable; + + $this->data = $data; + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/loca.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/loca.php new file mode 100644 index 0000000..bbd87d1 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/loca.php @@ -0,0 +1,79 @@ +getFont(); + $offset = $font->pos(); + + $indexToLocFormat = $font->getData("head", "indexToLocFormat"); + $numGlyphs = $font->getData("maxp", "numGlyphs"); + + $font->seek($offset); + + $data = array(); + + // 2 bytes + if ($indexToLocFormat == 0) { + $d = $font->read(($numGlyphs + 1) * 2); + $loc = unpack("n*", $d); + + for ($i = 0; $i <= $numGlyphs; $i++) { + $data[] = isset($loc[$i + 1]) ? $loc[$i + 1] * 2 : 0; + } + } + + // 4 bytes + else { + if ($indexToLocFormat == 1) { + $d = $font->read(($numGlyphs + 1) * 4); + $loc = unpack("N*", $d); + + for ($i = 0; $i <= $numGlyphs; $i++) { + $data[] = isset($loc[$i + 1]) ? $loc[$i + 1] : 0; + } + } + } + + $this->data = $data; + } + + function _encode() { + $font = $this->getFont(); + $data = $this->data; + + $indexToLocFormat = $font->getData("head", "indexToLocFormat"); + $numGlyphs = $font->getData("maxp", "numGlyphs"); + $length = 0; + + // 2 bytes + if ($indexToLocFormat == 0) { + for ($i = 0; $i <= $numGlyphs; $i++) { + $length += $font->writeUInt16($data[$i] / 2); + } + } + + // 4 bytes + else { + if ($indexToLocFormat == 1) { + for ($i = 0; $i <= $numGlyphs; $i++) { + $length += $font->writeUInt32($data[$i]); + } + } + } + + return $length; + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/maxp.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/maxp.php new file mode 100644 index 0000000..867125a --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/maxp.php @@ -0,0 +1,41 @@ + self::Fixed, + "numGlyphs" => self::uint16, + "maxPoints" => self::uint16, + "maxContours" => self::uint16, + "maxComponentPoints" => self::uint16, + "maxComponentContours" => self::uint16, + "maxZones" => self::uint16, + "maxTwilightPoints" => self::uint16, + "maxStorage" => self::uint16, + "maxFunctionDefs" => self::uint16, + "maxInstructionDefs" => self::uint16, + "maxStackElements" => self::uint16, + "maxSizeOfInstructions" => self::uint16, + "maxComponentElements" => self::uint16, + "maxComponentDepth" => self::uint16, + ); + + function _encode() { + $font = $this->getFont(); + $this->data["numGlyphs"] = count($font->getSubset()); + + return parent::_encode(); + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/name.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/name.php new file mode 100644 index 0000000..003a1e8 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/name.php @@ -0,0 +1,241 @@ + self::uint16, + "count" => self::uint16, + "stringOffset" => self::uint16, + ); + + const NAME_COPYRIGHT = 0; + const NAME_NAME = 1; + const NAME_SUBFAMILY = 2; + const NAME_SUBFAMILY_ID = 3; + const NAME_FULL_NAME = 4; + const NAME_VERSION = 5; + const NAME_POSTSCRIPT_NAME = 6; + const NAME_TRADEMARK = 7; + const NAME_MANUFACTURER = 8; + const NAME_DESIGNER = 9; + const NAME_DESCRIPTION = 10; + const NAME_VENDOR_URL = 11; + const NAME_DESIGNER_URL = 12; + const NAME_LICENSE = 13; + const NAME_LICENSE_URL = 14; + const NAME_PREFERRE_FAMILY = 16; + const NAME_PREFERRE_SUBFAMILY = 17; + const NAME_COMPAT_FULL_NAME = 18; + const NAME_SAMPLE_TEXT = 19; + + static $nameIdCodes = array( + 0 => "Copyright", + 1 => "FontName", + 2 => "FontSubfamily", + 3 => "UniqueID", + 4 => "FullName", + 5 => "Version", + 6 => "PostScriptName", + 7 => "Trademark", + 8 => "Manufacturer", + 9 => "Designer", + 10 => "Description", + 11 => "FontVendorURL", + 12 => "FontDesignerURL", + 13 => "LicenseDescription", + 14 => "LicenseURL", + // 15 + 16 => "PreferredFamily", + 17 => "PreferredSubfamily", + 18 => "CompatibleFullName", + 19 => "SampleText", + ); + + static $platforms = array( + 0 => "Unicode", + 1 => "Macintosh", + // 2 => Reserved + 3 => "Microsoft", + ); + + static $platformSpecific = array( + // Unicode + 0 => array( + 0 => "Default semantics", + 1 => "Version 1.1 semantics", + 2 => "ISO 10646 1993 semantics (deprecated)", + 3 => "Unicode 2.0 or later semantics", + ), + + // Macintosh + 1 => array( + 0 => "Roman", + 1 => "Japanese", + 2 => "Traditional Chinese", + 3 => "Korean", + 4 => "Arabic", + 5 => "Hebrew", + 6 => "Greek", + 7 => "Russian", + 8 => "RSymbol", + 9 => "Devanagari", + 10 => "Gurmukhi", + 11 => "Gujarati", + 12 => "Oriya", + 13 => "Bengali", + 14 => "Tamil", + 15 => "Telugu", + 16 => "Kannada", + 17 => "Malayalam", + 18 => "Sinhalese", + 19 => "Burmese", + 20 => "Khmer", + 21 => "Thai", + 22 => "Laotian", + 23 => "Georgian", + 24 => "Armenian", + 25 => "Simplified Chinese", + 26 => "Tibetan", + 27 => "Mongolian", + 28 => "Geez", + 29 => "Slavic", + 30 => "Vietnamese", + 31 => "Sindhi", + ), + + // Microsoft + 3 => array( + 0 => "Symbol", + 1 => "Unicode BMP (UCS-2)", + 2 => "ShiftJIS", + 3 => "PRC", + 4 => "Big5", + 5 => "Wansung", + 6 => "Johab", + // 7 => Reserved + // 8 => Reserved + // 9 => Reserved + 10 => "Unicode UCS-4", + ), + ); + + protected function _parse() { + $font = $this->getFont(); + + $tableOffset = $font->pos(); + + $data = $font->unpack(self::$header_format); + + $records = array(); + for ($i = 0; $i < $data["count"]; $i++) { + $record = new nameRecord(); + $record_data = $font->unpack(nameRecord::$format); + $record->map($record_data); + + $records[] = $record; + } + + $system_encodings = mb_list_encodings(); + $system_encodings = array_change_key_case(array_fill_keys($system_encodings, true), CASE_UPPER); + + $names = array(); + foreach ($records as $record) { + $font->seek($tableOffset + $data["stringOffset"] + $record->offset); + $record->stringRaw = $font->read($record->length); + + $encoding = null; + switch ($record->platformID) { + case 3: + switch ($record->platformSpecificID) { + case 2: + if (\array_key_exists("SJIS", $system_encodings)) { + $encoding = "SJIS"; + } + break; + case 3: + if (\array_key_exists("GB18030", $system_encodings)) { + $encoding = "GB18030"; + } + break; + case 4: + if (\array_key_exists("BIG-5", $system_encodings)) { + $encoding = "BIG-5"; + } + break; + case 5: + if (\array_key_exists("UHC", $system_encodings)) { + $encoding = "UHC"; + } + break; + } + break; + } + if ($encoding === null) { + $encoding = "UTF-16"; + } + + $record->string = mb_convert_encoding($record->stringRaw, "UTF-8", $encoding); + if (strpos($record->string, "\0") !== false) { + $record->string = str_replace("\0", "", $record->string); + } + $names[$record->nameID] = $record; + } + + $data["records"] = $names; + + $this->data = $data; + } + + protected function _encode() { + $font = $this->getFont(); + + /** @var nameRecord[] $records */ + $records = $this->data["records"]; + $count_records = \count($records); + + $this->data["count"] = $count_records; + $this->data["stringOffset"] = 6 + ($count_records * 12); // 6 => uint16 * 3, 12 => sizeof self::$record_format + + $length = $font->pack(self::$header_format, $this->data); + + $offset = 0; + + /** @var nameRecord[] $records_to_encode */ + $records_to_encode = array(); + foreach ($records as $record) { + $encoded_record = new nameRecord(); + $encoded_record->platformID = 3; + $encoded_record->platformSpecificID = 1; + $encoded_record->languageID = $record->languageID; + $encoded_record->nameID = $record->nameID; + $encoded_record->offset = $offset; + $encoded_record->string = $record->string; + $encoded_record->length = mb_strlen($encoded_record->getUTF16(), "8bit"); + $records_to_encode[] = $encoded_record; + + $offset += $encoded_record->length; + $length += $font->pack(nameRecord::$format, (array)$encoded_record); + } + + foreach ($records_to_encode as $record) { + $str = $record->getUTF16(); + $length += $font->write($str, mb_strlen($str, "8bit")); + } + + return $length; + } +} diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/nameRecord.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/nameRecord.php new file mode 100644 index 0000000..162629d --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/nameRecord.php @@ -0,0 +1,53 @@ + self::uint16, + "platformSpecificID" => self::uint16, + "languageID" => self::uint16, + "nameID" => self::uint16, + "length" => self::uint16, + "offset" => self::uint16, + ); + + public function map($data) { + foreach ($data as $key => $value) { + $this->$key = $value; + } + } + + public function getUTF8() { + return $this->string; + } + + public function getUTF16() { + return Font::UTF8ToUTF16($this->string); + } + + function __toString() { + return $this->string; + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/os2.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/os2.php new file mode 100644 index 0000000..b1a2016 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/os2.php @@ -0,0 +1,46 @@ + self::uint16, + "xAvgCharWidth" => self::int16, + "usWeightClass" => self::uint16, + "usWidthClass" => self::uint16, + "fsType" => self::int16, + "ySubscriptXSize" => self::int16, + "ySubscriptYSize" => self::int16, + "ySubscriptXOffset" => self::int16, + "ySubscriptYOffset" => self::int16, + "ySuperscriptXSize" => self::int16, + "ySuperscriptYSize" => self::int16, + "ySuperscriptXOffset" => self::int16, + "ySuperscriptYOffset" => self::int16, + "yStrikeoutSize" => self::int16, + "yStrikeoutPosition" => self::int16, + "sFamilyClass" => self::int16, + "panose" => array(self::uint8, 10), + "ulCharRange" => array(self::uint32, 4), + "achVendID" => array(self::char, 4), + "fsSelection" => self::uint16, + "fsFirstCharIndex" => self::uint16, + "fsLastCharIndex" => self::uint16, + "typoAscender" => self::int16, + "typoDescender" => self::int16, + "typoLineGap" => self::int16, + "winAscent" => self::int16, + "winDescent" => self::int16, + ); +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/post.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/post.php new file mode 100644 index 0000000..3b29c08 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/post.php @@ -0,0 +1,142 @@ + self::Fixed, + "italicAngle" => self::Fixed, + "underlinePosition" => self::FWord, + "underlineThickness" => self::FWord, + "isFixedPitch" => self::uint32, + "minMemType42" => self::uint32, + "maxMemType42" => self::uint32, + "minMemType1" => self::uint32, + "maxMemType1" => self::uint32, + ); + + protected function _parse() { + $font = $this->getFont(); + $data = $font->unpack($this->def); + + $names = array(); + + switch ($data["format"]) { + case 1: + $names = File::$macCharNames; + break; + + case 2: + $data["numberOfGlyphs"] = $font->readUInt16(); + + $glyphNameIndex = $font->readUInt16Many($data["numberOfGlyphs"]); + + $data["glyphNameIndex"] = $glyphNameIndex; + + $namesPascal = array(); + for ($i = 0; $i < $data["numberOfGlyphs"]; $i++) { + $len = $font->readUInt8(); + $namesPascal[] = $font->read($len); + } + + foreach ($glyphNameIndex as $g => $index) { + if ($index < 258) { + $names[$g] = File::$macCharNames[$index]; + } + else { + if (array_key_exists($index - 258, $namesPascal)) { + $names[$g] = $namesPascal[$index - 258]; + } + } + } + + break; + + case 2.5: + // TODO + break; + + case 3: + // nothing + break; + + case 4: + // TODO + break; + } + + $data["names"] = $names; + + $this->data = $data; + } + + function _encode() { + $font = $this->getFont(); + $data = $this->data; + $data["format"] = 3; + + $length = $font->pack($this->def, $data); + + return $length; + /* + $subset = $font->getSubset(); + + switch($data["format"]) { + case 1: + // nothing to do + break; + + case 2: + $old_names = $data["names"]; + + $glyphNameIndex = range(0, count($subset)); + + $names = array(); + foreach($subset as $gid) { + $names[] = $data["names"][$data["glyphNameIndex"][$gid]]; + } + + $numberOfGlyphs = count($names); + $length += $font->writeUInt16($numberOfGlyphs); + + foreach($glyphNameIndex as $gni) { + $length += $font->writeUInt16($gni); + } + + //$names = array_slice($names, 257); + foreach($names as $name) { + $len = strlen($name); + $length += $font->writeUInt8($len); + $length += $font->write($name, $len); + } + + break; + + case 2.5: + // TODO + break; + + case 3: + // nothing + break; + + case 4: + // TODO + break; + } + + return $length;*/ + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/prep.php b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/prep.php new file mode 100644 index 0000000..a8442cf --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/Table/Type/prep.php @@ -0,0 +1,29 @@ +getFont(); + $font->seek($this->entry->offset); + $this->rawData = $font->read($this->entry->length); + } + function _encode() { + return $this->getFont()->write($this->rawData, $this->entry->length); + } +} diff --git a/vendor/dompdf/php-font-lib/src/FontLib/TrueType/Collection.php b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/Collection.php new file mode 100644 index 0000000..2c97030 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/Collection.php @@ -0,0 +1,99 @@ +numFonts)) { + return; + } + + $this->read(4); // tag name + + $this->version = $this->readFixed(); + $this->numFonts = $this->readUInt32(); + + for ($i = 0; $i < $this->numFonts; $i++) { + $this->collectionOffsets[] = $this->readUInt32(); + } + } + + /** + * @param int $fontId + * + * @throws OutOfBoundsException + * @return File + */ + function getFont($fontId) { + $this->parse(); + + if (!isset($this->collectionOffsets[$fontId])) { + throw new OutOfBoundsException(); + } + + if (isset($this->collection[$fontId])) { + return $this->collection[$fontId]; + } + + $font = new File(); + $font->f = $this->f; + $font->setTableOffset($this->collectionOffsets[$fontId]); + + return $this->collection[$fontId] = $font; + } + + function current() { + return $this->getFont($this->position); + } + + function key() { + return $this->position; + } + + function next() { + return ++$this->position; + } + + function rewind() { + $this->position = 0; + } + + function valid() { + $this->parse(); + + return isset($this->collectionOffsets[$this->position]); + } + + function count() { + $this->parse(); + + return $this->numFonts; + } +} diff --git a/vendor/dompdf/php-font-lib/src/FontLib/TrueType/File.php b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/File.php new file mode 100644 index 0000000..f4c797a --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/File.php @@ -0,0 +1,590 @@ +> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN) + $bytes[] = ($o - 0xC0) << 0x06; + $numbytes = 2; + } elseif (($o >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN) + $bytes[] = ($o - 0xE0) << 0x0C; + $numbytes = 3; + } elseif (($o >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN) + $bytes[] = ($o - 0xF0) << 0x12; + $numbytes = 4; + } else { + $ord = false; + break; + } + } elseif (($o >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN + $bytes[] = $o - 0x80; + if (\count($bytes) === $numbytes) { + // compose UTF-8 bytes to a single unicode value + $o = $bytes[0]; + for ($j = 1; $j < $numbytes; $j++) { + $o += ($bytes[$j] << (($numbytes - $j - 1) * 0x06)); + } + if ((($o >= 0xD800) and ($o <= 0xDFFF)) or ($o >= 0x10FFFF)) { + // The definition of UTF-8 prohibits encoding character numbers between + // U+D800 and U+DFFF, which are reserved for use with the UTF-16 + // encoding form (as surrogate pairs) and do not directly represent + // characters. + return false; + } else { + $ord = $o; // add char to array + } + // reset data for next char + $bytes = []; + $numbytes = 1; + } + } else { + $ord = false; + break; + } + } + + return $ord; + } + + function getTable() { + $this->parseTableEntries(); + + return $this->directory; + } + + function setTableOffset($offset) { + $this->tableOffset = $offset; + } + + function parse() { + $this->parseTableEntries(); + + $this->data = array(); + + foreach ($this->directory as $tag => $table) { + if (empty($this->data[$tag])) { + $this->readTable($tag); + } + } + } + + function utf8toUnicode($str) { + $len = mb_strlen($str, '8bit'); + $out = array(); + + for ($i = 0; $i < $len; $i++) { + $uni = -1; + $h = ord($str[$i]); + + if ($h <= 0x7F) { + $uni = $h; + } + elseif ($h >= 0xC2) { + if (($h <= 0xDF) && ($i < $len - 1)) { + $uni = ($h & 0x1F) << 6 | (ord($str[++$i]) & 0x3F); + } + elseif (($h <= 0xEF) && ($i < $len - 2)) { + $uni = ($h & 0x0F) << 12 | (ord($str[++$i]) & 0x3F) << 6 | (ord($str[++$i]) & 0x3F); + } + elseif (($h <= 0xF4) && ($i < $len - 3)) { + $uni = ($h & 0x0F) << 18 | (ord($str[++$i]) & 0x3F) << 12 | (ord($str[++$i]) & 0x3F) << 6 | (ord($str[++$i]) & 0x3F); + } + } + + if ($uni >= 0) { + $out[] = $uni; + } + } + + return $out; + } + + function getUnicodeCharMap() { + $subtable = null; + foreach ($this->getData("cmap", "subtables") as $_subtable) { + if ($_subtable["platformID"] == 0 || ($_subtable["platformID"] == 3 && $_subtable["platformSpecificID"] == 1)) { + $subtable = $_subtable; + break; + } + } + + if ($subtable) { + return $subtable["glyphIndexArray"]; + } + + $system_encodings = mb_list_encodings(); + $system_encodings = array_change_key_case(array_fill_keys($system_encodings, true), CASE_UPPER); + foreach ($this->getData("cmap", "subtables") as $_subtable) { + $encoding = null; + switch ($_subtable["platformID"]) { + case 3: + switch ($_subtable["platformSpecificID"]) { + case 2: + if (\array_key_exists("SJIS", $system_encodings)) { + $encoding = "SJIS"; + } + break; + case 3: + if (\array_key_exists("GB18030", $system_encodings)) { + $encoding = "GB18030"; + } + break; + case 4: + if (\array_key_exists("BIG-5", $system_encodings)) { + $encoding = "BIG-5"; + } + break; + case 5: + if (\array_key_exists("UHC", $system_encodings)) { + $encoding = "UHC"; + } + break; + } + break; + } + if ($encoding) { + $glyphIndexArray = array(); + foreach ($_subtable["glyphIndexArray"] as $c => $gid) { + $str = trim(pack("N", $c)); + if (\strlen($str) > 0) { + $ord = $this->uniord($str, $encoding); + if ($ord > 0) { + $glyphIndexArray[$ord] = $gid; + } + } + } + return $glyphIndexArray; + } + } + + return null; + } + + function setSubset($subset) { + if (!is_array($subset)) { + $subset = $this->utf8toUnicode($subset); + } + + $subset = array_unique($subset); + + $glyphIndexArray = $this->getUnicodeCharMap(); + + if (!$glyphIndexArray) { + return; + } + + $gids = array( + 0, // .notdef + 1, // .null + ); + + foreach ($subset as $code) { + if (!isset($glyphIndexArray[$code])) { + continue; + } + + $gid = $glyphIndexArray[$code]; + $gids[$gid] = $gid; + } + + /** @var glyf $glyf */ + $glyf = $this->getTableObject("glyf"); + if ($glyf) { + $gids = $glyf->getGlyphIDs($gids); + sort($gids); + $this->glyph_subset = $gids; + } + $this->glyph_all = array_values($glyphIndexArray); // FIXME + } + + function getSubset() { + if (empty($this->glyph_subset)) { + return $this->glyph_all; + } + + return $this->glyph_subset; + } + + function encode($tags = array()) { + if (!self::$raw) { + $tags = array_merge(array("head", "hhea", "cmap", "hmtx", "maxp", "glyf", "loca", "name", "post", "cvt ", "fpgm", "prep"), $tags); + } + else { + $tags = array_keys($this->directory); + } + + $n = 16; // @todo + + Font::d("Tables : " . implode(", ", $tags)); + + /** @var DirectoryEntry[] $entries */ + $entries = array(); + foreach ($tags as $tag) { + if (!isset($this->directory[$tag])) { + Font::d(" >> '$tag' table doesn't exist"); + continue; + } + + $entries[$tag] = $this->directory[$tag]; + } + + $num_tables = count($entries); + $exponent = floor(log($num_tables, 2)); + $power_of_two = pow(2, $exponent); + + $this->header->data["numTables"] = $num_tables; + $this->header->data["searchRange"] = $power_of_two * 16; + $this->header->data["entrySelector"] = log($power_of_two, 2); + $this->header->data["rangeShift"] = $num_tables * 16 - $this->header->data["searchRange"]; + $this->header->encode(); + + $directory_offset = $this->pos(); + $offset = $directory_offset + $num_tables * $n; + $this->seek($offset); + + $i = 0; + foreach ($entries as $entry) { + $entry->encode($directory_offset + $i * $n); + $i++; + } + } + + function parseHeader() { + if (!empty($this->header)) { + return; + } + + $this->seek($this->tableOffset); + + $this->header = new Header($this); + $this->header->parse(); + } + + function getFontType(){ + $class_parts = explode("\\", get_class($this)); + return $class_parts[1]; + } + + function parseTableEntries() { + $this->parseHeader(); + + if (!empty($this->directory)) { + return; + } + + if (empty($this->header->data["numTables"])) { + return; + } + + + $type = $this->getFontType(); + $class = "FontLib\\$type\\TableDirectoryEntry"; + + for ($i = 0; $i < $this->header->data["numTables"]; $i++) { + /** @var TableDirectoryEntry $entry */ + $entry = new $class($this); + $entry->parse(); + + $this->directory[$entry->tag] = $entry; + } + } + + function normalizeFUnit($value, $base = 1000) { + return round($value * ($base / $this->getData("head", "unitsPerEm"))); + } + + protected function readTable($tag) { + $this->parseTableEntries(); + + if (!self::$raw) { + $name_canon = preg_replace("/[^a-z0-9]/", "", strtolower($tag)); + + $class = "FontLib\\Table\\Type\\$name_canon"; + + if (!isset($this->directory[$tag]) || !@class_exists($class)) { + return; + } + } + else { + $class = "FontLib\\Table\\Table"; + } + + /** @var Table $table */ + $table = new $class($this->directory[$tag]); + $table->parse(); + + $this->data[$tag] = $table; + } + + /** + * @param $name + * + * @return Table + */ + public function getTableObject($name) { + if (\array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + return null; + } + + public function setTableObject($name, Table $data) { + $this->data[$name] = $data; + } + + public function getData($name, $key = null) { + $this->parseTableEntries(); + + if (empty($this->data[$name])) { + $this->readTable($name); + } + + if (!isset($this->data[$name])) { + return null; + } + + if (!$key) { + return $this->data[$name]->data; + } + else { + return $this->data[$name]->data[$key]; + } + } + + function addDirectoryEntry(DirectoryEntry $entry) { + $this->directory[$entry->tag] = $entry; + } + + function saveAdobeFontMetrics($file, $encoding = null) { + $afm = new AdobeFontMetrics($this); + $afm->write($file, $encoding); + } + + /** + * Get a specific name table string value from its ID + * + * @param int $nameID The name ID + * + * @return string|null + */ + function getNameTableString($nameID) { + /** @var nameRecord[] $records */ + $records = $this->getData("name", "records"); + + if (!isset($records[$nameID])) { + return null; + } + + return $records[$nameID]->string; + } + + /** + * Get font copyright + * + * @return string|null + */ + function getFontCopyright() { + return $this->getNameTableString(name::NAME_COPYRIGHT); + } + + /** + * Get font name + * + * @return string|null + */ + function getFontName() { + return $this->getNameTableString(name::NAME_NAME); + } + + /** + * Get font subfamily + * + * @return string|null + */ + function getFontSubfamily() { + return $this->getNameTableString(name::NAME_SUBFAMILY); + } + + /** + * Get font subfamily ID + * + * @return string|null + */ + function getFontSubfamilyID() { + return $this->getNameTableString(name::NAME_SUBFAMILY_ID); + } + + /** + * Get font full name + * + * @return string|null + */ + function getFontFullName() { + return $this->getNameTableString(name::NAME_FULL_NAME); + } + + /** + * Get font version + * + * @return string|null + */ + function getFontVersion() { + return $this->getNameTableString(name::NAME_VERSION); + } + + /** + * Get font weight + * + * @return string|null + */ + function getFontWeight() { + return $this->getTableObject("OS/2")->data["usWeightClass"]; + } + + /** + * Get font Postscript name + * + * @return string|null + */ + function getFontPostscriptName() { + return $this->getNameTableString(name::NAME_POSTSCRIPT_NAME); + } + + function reduce() { + $names_to_keep = array( + name::NAME_COPYRIGHT, + name::NAME_NAME, + name::NAME_SUBFAMILY, + name::NAME_SUBFAMILY_ID, + name::NAME_FULL_NAME, + name::NAME_VERSION, + name::NAME_POSTSCRIPT_NAME, + ); + + foreach ($this->data["name"]->data["records"] as $id => $rec) { + if (!in_array($id, $names_to_keep)) { + unset($this->data["name"]->data["records"][$id]); + } + } + } +} diff --git a/vendor/dompdf/php-font-lib/src/FontLib/TrueType/Header.php b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/Header.php new file mode 100644 index 0000000..7c04728 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/Header.php @@ -0,0 +1,30 @@ + self::uint32, + "numTables" => self::uint16, + "searchRange" => self::uint16, + "entrySelector" => self::uint16, + "rangeShift" => self::uint16, + ); + + public function parse() { + parent::parse(); + + $format = $this->data["format"]; + $this->data["formatText"] = $this->convertUInt32ToStr($format); + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/TrueType/TableDirectoryEntry.php b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/TableDirectoryEntry.php new file mode 100644 index 0000000..3ddd6d0 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/TrueType/TableDirectoryEntry.php @@ -0,0 +1,32 @@ +font; + $this->checksum = $font->readUInt32(); + $this->offset = $font->readUInt32(); + $this->length = $font->readUInt32(); + $this->entryLength += 12; + } +} + diff --git a/vendor/dompdf/php-font-lib/src/FontLib/WOFF/File.php b/vendor/dompdf/php-font-lib/src/FontLib/WOFF/File.php new file mode 100644 index 0000000..bbc40fb --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/WOFF/File.php @@ -0,0 +1,80 @@ +header)) { + return; + } + + $this->header = new Header($this); + $this->header->parse(); + } + + public function load($file) { + parent::load($file); + + $this->parseTableEntries(); + $dataOffset = $this->pos() + count($this->directory) * 20; + + $fw = $this->getTempFile(false); + $fr = $this->f; + + $this->f = $fw; + $offset = $this->header->encode(); + + foreach ($this->directory as $entry) { + // Read ... + $this->f = $fr; + $this->seek($entry->offset); + $data = $this->read($entry->length); + + if ($entry->length < $entry->origLength) { + $data = (string) gzuncompress($data); + } + + // Prepare data ... + $length = mb_strlen($data, '8bit'); + $entry->length = $entry->origLength = $length; + $entry->offset = $dataOffset; + + // Write ... + $this->f = $fw; + + // Woff Entry + $this->seek($offset); + $offset += $this->write($entry->tag, 4); // tag + $offset += $this->writeUInt32($dataOffset); // offset + $offset += $this->writeUInt32($length); // length + $offset += $this->writeUInt32($length); // origLength + $offset += $this->writeUInt32(DirectoryEntry::computeChecksum($data)); // checksum + + // Data + $this->seek($dataOffset); + $dataOffset += $this->write($data, $length); + } + + $this->f = $fw; + $this->seek(0); + + // Need to re-parse this, don't know why + $this->header = null; + $this->directory = array(); + $this->parseTableEntries(); + } +} diff --git a/vendor/dompdf/php-font-lib/src/FontLib/WOFF/Header.php b/vendor/dompdf/php-font-lib/src/FontLib/WOFF/Header.php new file mode 100644 index 0000000..e87a8d3 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/WOFF/Header.php @@ -0,0 +1,31 @@ + self::uint32, + "flavor" => self::uint32, + "length" => self::uint32, + "numTables" => self::uint16, + self::uint16, + "totalSfntSize" => self::uint32, + "majorVersion" => self::uint16, + "minorVersion" => self::uint16, + "metaOffset" => self::uint32, + "metaLength" => self::uint32, + "metaOrigLength" => self::uint32, + "privOffset" => self::uint32, + "privLength" => self::uint32, + ); +} \ No newline at end of file diff --git a/vendor/dompdf/php-font-lib/src/FontLib/WOFF/TableDirectoryEntry.php b/vendor/dompdf/php-font-lib/src/FontLib/WOFF/TableDirectoryEntry.php new file mode 100644 index 0000000..b57a650 --- /dev/null +++ b/vendor/dompdf/php-font-lib/src/FontLib/WOFF/TableDirectoryEntry.php @@ -0,0 +1,33 @@ +font; + $this->offset = $font->readUInt32(); + $this->length = $font->readUInt32(); + $this->origLength = $font->readUInt32(); + $this->checksum = $font->readUInt32(); + } +} diff --git a/vendor/dompdf/php-svg-lib/AUTHORS.md b/vendor/dompdf/php-svg-lib/AUTHORS.md new file mode 100644 index 0000000..6642e3d --- /dev/null +++ b/vendor/dompdf/php-svg-lib/AUTHORS.md @@ -0,0 +1,17 @@ +SvgLib was designed and developed by Fabien Ménager. + +### Current Team + +* **Brian Sweeney** (maintainer) + +### Alumni + +* **Fabien Ménager** (creator) + +### Contributors +* **ssddanbrown** +* [and many more...](https://github.com/dompdf/php-svg-lib/graphs/contributors) + +### Thanks + +SvgLib would not have been possible without strong community support. \ No newline at end of file diff --git a/vendor/dompdf/php-svg-lib/LICENSE b/vendor/dompdf/php-svg-lib/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/vendor/dompdf/php-svg-lib/README.md b/vendor/dompdf/php-svg-lib/README.md new file mode 100644 index 0000000..ff3c921 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/README.md @@ -0,0 +1,12 @@ +# SVG file parsing / rendering library + +[![Build Status](https://github.com/dompdf/php-svg-lib/actions/workflows/test.yml/badge.svg)](https://github.com/dompdf/php-svg-lib/actions/workflows/test.yml) + +[![Latest Stable Version](https://poser.pugx.org/phenx/php-svg-lib/v/stable)](https://packagist.org/packages/phenx/php-svg-lib) +[![Total Downloads](https://poser.pugx.org/phenx/php-svg-lib/downloads)](https://packagist.org/packages/phenx/php-svg-lib) +[![Latest Unstable Version](https://poser.pugx.org/phenx/php-svg-lib/v/unstable)](https://packagist.org/packages/phenx/php-svg-lib) +[![License](https://poser.pugx.org/phenx/php-svg-lib/license)](https://packagist.org/packages/phenx/php-svg-lib) + +The main purpose of this lib is to rasterize SVG to a surface which can be an image or a PDF for example, through a `\Svg\Surface` PHP interface. + +This project was initialized by the need to render SVG documents inside PDF files for the [DomPdf](https://github.com/dompdf/dompdf) project. diff --git a/vendor/dompdf/php-svg-lib/composer.json b/vendor/dompdf/php-svg-lib/composer.json new file mode 100644 index 0000000..bf2b153 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/composer.json @@ -0,0 +1,31 @@ +{ + "name": "dompdf/php-svg-lib", + "type": "library", + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "license": "LGPL-3.0-or-later", + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "autoload-dev": { + "psr-4": { + "Svg\\Tests\\": "tests/Svg" + } + }, + "require": { + "php": "^7.1 || ^8.0", + "ext-mbstring": "*", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/CssLength.php b/vendor/dompdf/php-svg-lib/src/Svg/CssLength.php new file mode 100644 index 0000000..88eda8c --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/CssLength.php @@ -0,0 +1,135 @@ + + */ + protected static $inchDivisions = [ + 'in' => 1, + 'cm' => 2.54, + 'mm' => 25.4, + 'q' => 101.6, + 'pc' => 6, + 'pt' => 72, + ]; + + /** + * The CSS length unit indicator. + * Will be lower-case and one of the units listed in the '$units' array or empty. + * + * @var string + */ + protected $unit = ''; + + /** + * The numeric value of the given length. + * + * @var float + */ + protected $value = 0; + + /** + * The original unparsed length provided. + * + * @var string + */ + protected $unparsed; + + public function __construct(string $length) + { + $this->unparsed = $length; + $this->parseLengthComponents($length); + } + + /** + * Parse out the unit and value components from the given string length. + */ + protected function parseLengthComponents(string $length): void + { + $length = strtolower($length); + + foreach (self::$units as $unit) { + $pos = strpos($length, $unit); + if ($pos) { + $this->value = floatval(substr($length, 0, $pos)); + $this->unit = $unit; + return; + } + } + + $this->unit = ''; + $this->value = floatval($length); + } + + /** + * Get the unit type of this css length. + * Units are standardised to be lower-cased. + * + * @return string + */ + public function getUnit(): string + { + return $this->unit; + } + + /** + * Get this CSS length in the equivalent pixel count size. + * + * @param float $referenceSize + * @param float $dpi + * + * @return float + */ + public function toPixels(float $referenceSize = 11.0, float $dpi = 96.0): float + { + // Standard relative units + if (in_array($this->unit, ['em', 'rem', 'ex', 'ch'])) { + return $this->value * $referenceSize; + } + + // Percentage relative units + if (in_array($this->unit, ['%', 'vw', 'vh', 'vmin', 'vmax'])) { + return $this->value * ($referenceSize / 100); + } + + // Inch relative units + if (in_array($this->unit, array_keys(static::$inchDivisions))) { + $inchValue = $this->value * $dpi; + $division = static::$inchDivisions[$this->unit]; + return $inchValue / $division; + } + + return $this->value; + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-svg-lib/src/Svg/DefaultStyle.php b/vendor/dompdf/php-svg-lib/src/Svg/DefaultStyle.php new file mode 100644 index 0000000..e27bdb9 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/DefaultStyle.php @@ -0,0 +1,28 @@ +filename = $filename; + } + + protected function initParser() { + $parser = xml_parser_create("utf-8"); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); + xml_set_element_handler( + $parser, + array($this, "_tagStart"), + array($this, "_tagEnd") + ); + xml_set_character_data_handler( + $parser, + array($this, "_charData") + ); + + return $parser; + } + + public function __construct() { + + } + + /** + * Increase the nesting level for defs-like elements + * + * @return int + */ + public function enterDefs () { + $this->_defs_depth++; + $this->inDefs = true; + return $this->_defs_depth; + } + + /** + * Decrease the nesting level for defs-like elements + * + * @return int + */ + public function exitDefs () { + $this->_defs_depth--; + if ($this->_defs_depth < 0) { + $this->_defs_depth = 0; + } + $this->inDefs = ($this->_defs_depth > 0 ? true : false); + return $this->_defs_depth; + } + + /** + * @return SurfaceInterface + */ + public function getSurface() + { + return $this->surface; + } + + public function getStack() + { + return $this->stack; + } + + public function getWidth() + { + return $this->width; + } + + public function getHeight() + { + return $this->height; + } + + public function getDiagonal() + { + return sqrt(($this->width)**2 + ($this->height)**2) / sqrt(2); + } + + public function getDimensions() { + $rootAttributes = null; + + $parser = xml_parser_create("utf-8"); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); + xml_set_element_handler( + $parser, + function ($parser, $name, $attributes) use (&$rootAttributes) { + if ($name === "svg" && $rootAttributes === null) { + $attributes = array_change_key_case($attributes, CASE_LOWER); + + $rootAttributes = $attributes; + } + }, + function ($parser, $name) {} + ); + + $fp = fopen($this->filename, "r"); + while ($line = fread($fp, 8192)) { + xml_parse($parser, $line, false); + + if ($rootAttributes !== null) { + break; + } + } + xml_parse($parser, "", true); + + xml_parser_free($parser); + + return $this->handleSizeAttributes($rootAttributes); + } + + public function handleSizeAttributes($attributes){ + if ($this->width === null) { + if (isset($attributes["width"])) { + $width = $this->convertSize($attributes["width"], 400); + $this->width = $width; + } + + if (isset($attributes["height"])) { + $height = $this->convertSize($attributes["height"], 300); + $this->height = $height; + } + + if (isset($attributes['viewbox'])) { + $viewBox = preg_split('/[\s,]+/is', trim($attributes['viewbox'])); + if (count($viewBox) == 4) { + $this->x = $viewBox[0]; + $this->y = $viewBox[1]; + + if (!$this->width) { + $this->width = $viewBox[2]; + } + if (!$this->height) { + $this->height = $viewBox[3]; + } + } + } + } + + return array( + 0 => $this->width, + 1 => $this->height, + + "width" => $this->width, + "height" => $this->height, + ); + } + + public function getDocument(){ + return $this; + } + + /** + * Append a style sheet + * + * @param \Sabberworm\CSS\CSSList\Document $stylesheet + */ + public function appendStyleSheet($stylesheet) { + $this->styleSheets[] = $stylesheet; + } + + /** + * Get the document style sheets + * + * @return \Sabberworm\CSS\CSSList\Document[] + */ + public function getStyleSheets() { + return $this->styleSheets; + } + + protected function before($attributes) + { + $surface = $this->getSurface(); + + $style = new DefaultStyle($this); + $style->inherit($this); + $style->fromAttributes($attributes); + + $this->setStyle($style); + + $surface->setStyle($style); + } + + public function render(SurfaceInterface $surface) + { + $this->_defs_depth = 0; + $this->inDefs = false; + $this->surface = $surface; + + $parser = $this->initParser(); + + if ($this->x || $this->y) { + $surface->translate(-$this->x, -$this->y); + } + + $fp = fopen($this->filename, "r"); + while ($line = fread($fp, 8192)) { + xml_parse($parser, $line, false); + } + + xml_parse($parser, "", true); + + xml_parser_free($parser); + } + + protected function svgOffset($attributes) + { + $this->attributes = $attributes; + + $this->handleSizeAttributes($attributes); + } + + public function getDef($id) { + $id = ltrim($id, "#"); + + return isset($this->defs[$id]) ? $this->defs[$id] : null; + } + + private function _tagStart($parser, $name, $attributes) + { + $this->x = 0; + $this->y = 0; + + $tag = null; + + $attributes = array_change_key_case($attributes, CASE_LOWER); + + switch (strtolower($name)) { + case 'defs': + $this->enterDefs(); + return; + + case 'svg': + if (count($this->attributes)) { + $tag = new Group($this, $name); + } + else { + $tag = $this; + $this->svgOffset($attributes); + } + break; + + case 'path': + $tag = new Path($this, $name); + break; + + case 'rect': + $tag = new Rect($this, $name); + break; + + case 'circle': + $tag = new Circle($this, $name); + break; + + case 'ellipse': + $tag = new Ellipse($this, $name); + break; + + case 'image': + $tag = new Image($this, $name); + break; + + case 'line': + $tag = new Line($this, $name); + break; + + case 'polyline': + $tag = new Polyline($this, $name); + break; + + case 'polygon': + $tag = new Polygon($this, $name); + break; + + case 'lineargradient': + $tag = new LinearGradient($this, $name); + break; + + case 'radialgradient': + $tag = new LinearGradient($this, $name); + break; + + case 'stop': + $tag = new Stop($this, $name); + break; + + case 'style': + $tag = new StyleTag($this, $name); + break; + + case 'a': + $tag = new Anchor($this, $name); + break; + + case 'g': + $tag = new Group($this, $name); + break; + + case 'symbol': + $this->enterDefs(); + $tag = new Symbol($this, $name); + break; + + case 'clippath': + $tag = new ClipPath($this, $name); + break; + + case 'use': + $tag = new UseTag($this, $name); + break; + + case 'text': + $tag = new Text($this, $name); + break; + + case 'desc': + return; + } + + if ($tag) { + if (isset($attributes["id"])) { + $this->defs[$attributes["id"]] = $tag; + } + else { + /** @var AbstractTag $top */ + $top = end($this->stack); + if ($top && $top != $tag) { + $top->children[] = $tag; + } + } + + $this->stack[] = $tag; + + $tag->handle($attributes); + } + } + + function _charData($parser, $data) + { + $stack_top = end($this->stack); + + if ($stack_top instanceof Text || $stack_top instanceof StyleTag) { + $stack_top->appendText($data); + } + } + + function _tagEnd($parser, $name) + { + /** @var AbstractTag $tag */ + $tag = null; + switch (strtolower($name)) { + case 'defs': + $this->exitDefs(); + return; + + case 'symbol': + $this->exitDefs(); + $tag = array_pop($this->stack); + break; + + case 'svg': + case 'path': + case 'rect': + case 'circle': + case 'ellipse': + case 'image': + case 'line': + case 'polyline': + case 'polygon': + case 'radialgradient': + case 'lineargradient': + case 'stop': + case 'style': + case 'text': + case 'g': + case 'clippath': + case 'use': + case 'a': + $tag = array_pop($this->stack); + break; + } + + if ((!$this->inDefs && $tag) || $tag instanceof StyleTag) { + $tag->handleEnd(); + } + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Gradient/Stop.php b/vendor/dompdf/php-svg-lib/src/Svg/Gradient/Stop.php new file mode 100644 index 0000000..186cc57 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Gradient/Stop.php @@ -0,0 +1,15 @@ +_document = $document; + } + } + + protected function getStyleMap() + { + return array( + 'color' => array('color', self::TYPE_COLOR), + 'opacity' => array('opacity', self::TYPE_NUMBER), + 'display' => array('display', self::TYPE_NAME), + + 'fill' => array('fill', self::TYPE_COLOR), + 'fill-opacity' => array('fillOpacity', self::TYPE_NUMBER), + 'fill-rule' => array('fillRule', self::TYPE_NAME), + + 'stroke' => array('stroke', self::TYPE_COLOR), + 'stroke-dasharray' => array('strokeDasharray', self::TYPE_NAME), + 'stroke-dashoffset' => array('strokeDashoffset', self::TYPE_NUMBER), + 'stroke-linecap' => array('strokeLinecap', self::TYPE_NAME), + 'stroke-linejoin' => array('strokeLinejoin', self::TYPE_NAME), + 'stroke-miterlimit' => array('strokeMiterlimit', self::TYPE_NUMBER), + 'stroke-opacity' => array('strokeOpacity', self::TYPE_NUMBER), + 'stroke-width' => array('strokeWidth', self::TYPE_NUMBER), + + 'font-family' => array('fontFamily', self::TYPE_NAME), + 'font-size' => array('fontSize', self::TYPE_NUMBER), + 'font-weight' => array('fontWeight', self::TYPE_NAME), + 'font-style' => array('fontStyle', self::TYPE_NAME), + 'text-anchor' => array('textAnchor', self::TYPE_NAME), + ); + } + + /** + * @param $attributes + * + * @return Style + */ + public function fromAttributes($attributes) + { + $this->fillStyles($attributes); + + if (isset($attributes["style"])) { + $styles = self::parseCssStyle($attributes["style"]); + $this->fillStyles($styles); + } + } + + public function inherit(AbstractTag $tag) { + $group = $tag->getParentGroup(); + if ($group) { + $parent_style = $group->getStyle(); + $this->_parentStyle = $parent_style; + foreach ($parent_style as $_key => $_value) { + if ($_value !== null) { + $this->$_key = $_value; + } + } + } + } + + public function fromStyleSheets(AbstractTag $tag, $attributes) { + $class = isset($attributes["class"]) ? preg_split('/\s+/', trim($attributes["class"])) : null; + + $stylesheets = $tag->getDocument()->getStyleSheets(); + + $styles = array(); + + foreach ($stylesheets as $_sc) { + + /** @var \Sabberworm\CSS\RuleSet\DeclarationBlock $_decl */ + foreach ($_sc->getAllDeclarationBlocks() as $_decl) { + + /** @var \Sabberworm\CSS\Property\Selector $_selector */ + foreach ($_decl->getSelectors() as $_selector) { + $_selector = $_selector->getSelector(); + + // Match class name + if ($class !== null) { + foreach ($class as $_class) { + if ($_selector === ".$_class") { + /** @var \Sabberworm\CSS\Rule\Rule $_rule */ + foreach ($_decl->getRules() as $_rule) { + $styles[$_rule->getRule()] = $_rule->getValue() . ""; + } + + break 2; + } + } + } + + // Match tag name + if ($_selector === $tag->tagName) { + /** @var \Sabberworm\CSS\Rule\Rule $_rule */ + foreach ($_decl->getRules() as $_rule) { + $styles[$_rule->getRule()] = $_rule->getValue() . ""; + } + + break; + } + } + } + } + + $this->fillStyles($styles); + } + + protected function fillStyles($styles) + { + $style_map = $this->getStyleMap(); + foreach ($style_map as $from => $spec) { + if (isset($styles[$from])) { + list($to, $type) = $spec; + $value = null; + switch ($type) { + case self::TYPE_COLOR: + $value = self::parseColor($styles[$from]); + if ($value === "currentcolor") { + if ($type === "color") { + $value = $this->_parentStyle->color; + } else { + $value = $this->color; + } + } + if (is_array($value) && $value[3] !== 1.0 && array_key_exists("{$from}-opacity", $style_map) === true) { + $styles["{$from}-opacity"] = $value[3]; + } + break; + + case self::TYPE_NUMBER: + $value = ($styles[$from] === null) ? null : (float)$styles[$from]; + break; + + default: + $value = $styles[$from]; + } + + if ($from === "font-family") { + $scheme = \strtolower(parse_url($value, PHP_URL_SCHEME) ?: ""); + if ( + $scheme === "phar" || \strtolower(\substr($value, 0, 7)) === "phar://" + || ($this->_document !== null && $this->_document->allowExternalReferences === false && $scheme !== "data") + ) { + continue; + } + } + + if ($value !== null) { + $this->$to = $value; + } + } + } + } + + static function parseColor($color) + { + $color = strtolower(trim($color)); + + $parts = preg_split('/[^,]\s+/', $color, 2); + + if (count($parts) == 2) { + $color = $parts[1]; + } else { + $color = $parts[0]; + } + + if ($color === "none") { + return "none"; + } + + if ($color === "currentcolor") { + return "currentcolor"; + } + + if ($color === "transparent") { + return [0.0, 0.0, 0.0, 0.0]; + } + + // SVG color name + if (isset(self::$colorNames[$color])) { + return self::parseHexColor(self::$colorNames[$color]); + } + + // Hex color + if ($color[0] === "#") { + return self::parseHexColor($color); + } + + // RGB color + if (strpos($color, "rgb") !== false) { + return self::getQuad($color); + } + + // HSL color + if (strpos($color, "hsl") !== false) { + $quad = self::getQuad($color, true); + + if ($quad == null) { + return null; + } + + list($h, $s, $l, $a) = $quad; + + $r = $l; + $g = $l; + $b = $l; + $v = ($l <= 0.5) ? ($l * (1.0 + $s)) : ($l + $s - $l * $s); + if ($v > 0) { + $m = $l + $l - $v; + $sv = ($v - $m) / $v; + $h *= 6.0; + $sextant = floor($h); + $fract = $h - $sextant; + $vsf = $v * $sv * $fract; + $mid1 = $m + $vsf; + $mid2 = $v - $vsf; + + switch ($sextant) { + case 0: + $r = $v; + $g = $mid1; + $b = $m; + break; + case 1: + $r = $mid2; + $g = $v; + $b = $m; + break; + case 2: + $r = $m; + $g = $v; + $b = $mid1; + break; + case 3: + $r = $m; + $g = $mid2; + $b = $v; + break; + case 4: + $r = $mid1; + $g = $m; + $b = $v; + break; + case 5: + $r = $v; + $g = $m; + $b = $mid2; + break; + } + } + $a = $a * 255; + + return array( + $r * 255.0, + $g * 255.0, + $b * 255.0, + $a + ); + } + + // Gradient + if (strpos($color, "url(#") !== false) { + $i = strpos($color, "("); + $j = strpos($color, ")"); + + // Bad url format + if ($i === false || $j === false) { + return null; + } + + //FIXME: gradients not supported? + return null; // trim(substr($color, $i + 1, $j - $i - 1)); + } + + return null; + } + + static function getQuad($color, $percent = false) { + $i = strpos($color, "("); + $j = strpos($color, ")"); + + // Bad color value + if ($i === false || $j === false) { + return null; + } + + $quad = preg_split("/\\s*[,\\/]\\s*/", trim(substr($color, $i + 1, $j - $i - 1))); + if (!isset($quad[3])) { + $quad[3] = "1"; + } + + if (count($quad) != 3 && count($quad) != 4) { + return null; + } + + foreach (array_keys($quad) as $c) { + $quad[$c] = trim($quad[$c]); + + if ($percent) { + if ($quad[$c][strlen($quad[$c]) - 1] === "%") { + $quad[$c] = floatval($quad[$c]) / 100; + } else { + $quad[$c] = floatval($quad[$c]) / 255; + } + } else { + if ($quad[$c][strlen($quad[$c]) - 1] === "%") { + $quad[$c] = floatval($quad[$c]) * 2.55; + } else { + $quad[$c] = floatval($quad[$c]); + } + } + } + + return $quad; + } + + static function parseHexColor($hex) + { + $c = array(0.0, 0.0, 0.0, 1.0); + + // #FFFFFF + if (isset($hex[6])) { + $c[0] = hexdec(substr($hex, 1, 2)); + $c[1] = hexdec(substr($hex, 3, 2)); + $c[2] = hexdec(substr($hex, 5, 2)); + + if (isset($hex[7])) { + $alpha = substr($hex, 7, 2); + if (ctype_xdigit($alpha)) { + $c[3] = round(hexdec($alpha)/255, 2); + } + } + } else { + $c[0] = hexdec($hex[1] . $hex[1]); + $c[1] = hexdec($hex[2] . $hex[2]); + $c[2] = hexdec($hex[3] . $hex[3]); + + if (isset($hex[4])) { + if (ctype_xdigit($hex[4])) { + $c[3] = round(hexdec($hex[4] . $hex[4])/255, 2); + } + } + } + + return $c; + } + + /** + * Simple CSS parser + * + * @param $style + * + * @return array + */ + static function parseCssStyle($style) + { + $matches = array(); + preg_match_all("/([a-z-]+)\\s*:\\s*([^;$]+)/si", $style, $matches, PREG_SET_ORDER); + + $styles = array(); + foreach ($matches as $match) { + $styles[$match[1]] = $match[2]; + } + + return $styles; + } + + static $colorNames = array( + 'antiquewhite' => '#FAEBD7', + 'aqua' => '#00FFFF', + 'aquamarine' => '#7FFFD4', + 'beige' => '#F5F5DC', + 'black' => '#000000', + 'blue' => '#0000FF', + 'brown' => '#A52A2A', + 'cadetblue' => '#5F9EA0', + 'chocolate' => '#D2691E', + 'cornflowerblue' => '#6495ED', + 'crimson' => '#DC143C', + 'darkblue' => '#00008B', + 'darkgoldenrod' => '#B8860B', + 'darkgreen' => '#006400', + 'darkmagenta' => '#8B008B', + 'darkorange' => '#FF8C00', + 'darkred' => '#8B0000', + 'darkseagreen' => '#8FBC8F', + 'darkslategray' => '#2F4F4F', + 'darkviolet' => '#9400D3', + 'deepskyblue' => '#00BFFF', + 'dodgerblue' => '#1E90FF', + 'firebrick' => '#B22222', + 'forestgreen' => '#228B22', + 'fuchsia' => '#FF00FF', + 'gainsboro' => '#DCDCDC', + 'gold' => '#FFD700', + 'gray' => '#808080', + 'green' => '#008000', + 'greenyellow' => '#ADFF2F', + 'hotpink' => '#FF69B4', + 'indigo' => '#4B0082', + 'khaki' => '#F0E68C', + 'lavenderblush' => '#FFF0F5', + 'lemonchiffon' => '#FFFACD', + 'lightcoral' => '#F08080', + 'lightgoldenrodyellow' => '#FAFAD2', + 'lightgreen' => '#90EE90', + 'lightsalmon' => '#FFA07A', + 'lightskyblue' => '#87CEFA', + 'lightslategray' => '#778899', + 'lightyellow' => '#FFFFE0', + 'lime' => '#00FF00', + 'limegreen' => '#32CD32', + 'magenta' => '#FF00FF', + 'maroon' => '#800000', + 'mediumaquamarine' => '#66CDAA', + 'mediumorchid' => '#BA55D3', + 'mediumseagreen' => '#3CB371', + 'mediumspringgreen' => '#00FA9A', + 'mediumvioletred' => '#C71585', + 'midnightblue' => '#191970', + 'mintcream' => '#F5FFFA', + 'moccasin' => '#FFE4B5', + 'navy' => '#000080', + 'olive' => '#808000', + 'orange' => '#FFA500', + 'orchid' => '#DA70D6', + 'palegreen' => '#98FB98', + 'palevioletred' => '#D87093', + 'peachpuff' => '#FFDAB9', + 'pink' => '#FFC0CB', + 'powderblue' => '#B0E0E6', + 'purple' => '#800080', + 'red' => '#FF0000', + 'royalblue' => '#4169E1', + 'salmon' => '#FA8072', + 'seagreen' => '#2E8B57', + 'sienna' => '#A0522D', + 'silver' => '#C0C0C0', + 'skyblue' => '#87CEEB', + 'slategray' => '#708090', + 'springgreen' => '#00FF7F', + 'steelblue' => '#4682B4', + 'tan' => '#D2B48C', + 'teal' => '#008080', + 'thistle' => '#D8BFD8', + 'turquoise' => '#40E0D0', + 'violetred' => '#D02090', + 'white' => '#FFFFFF', + 'yellow' => '#FFFF00', + 'aliceblue' => '#f0f8ff', + 'azure' => '#f0ffff', + 'bisque' => '#ffe4c4', + 'blanchedalmond' => '#ffebcd', + 'blueviolet' => '#8a2be2', + 'burlywood' => '#deb887', + 'chartreuse' => '#7fff00', + 'coral' => '#ff7f50', + 'cornsilk' => '#fff8dc', + 'cyan' => '#00ffff', + 'darkcyan' => '#008b8b', + 'darkgray' => '#a9a9a9', + 'darkgrey' => '#a9a9a9', + 'darkkhaki' => '#bdb76b', + 'darkolivegreen' => '#556b2f', + 'darkorchid' => '#9932cc', + 'darksalmon' => '#e9967a', + 'darkslateblue' => '#483d8b', + 'darkslategrey' => '#2f4f4f', + 'darkturquoise' => '#00ced1', + 'deeppink' => '#ff1493', + 'dimgray' => '#696969', + 'dimgrey' => '#696969', + 'floralwhite' => '#fffaf0', + 'ghostwhite' => '#f8f8ff', + 'goldenrod' => '#daa520', + 'grey' => '#808080', + 'honeydew' => '#f0fff0', + 'indianred' => '#cd5c5c', + 'ivory' => '#fffff0', + 'lavender' => '#e6e6fa', + 'lawngreen' => '#7cfc00', + 'lightblue' => '#add8e6', + 'lightcyan' => '#e0ffff', + 'lightgray' => '#d3d3d3', + 'lightgrey' => '#d3d3d3', + 'lightpink' => '#ffb6c1', + 'lightseagreen' => '#20b2aa', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#b0c4de', + 'linen' => '#faf0e6', + 'mediumblue' => '#0000cd', + 'mediumpurple' => '#9370db', + 'mediumslateblue' => '#7b68ee', + 'mediumturquoise' => '#48d1cc', + 'mistyrose' => '#ffe4e1', + 'navajowhite' => '#ffdead', + 'oldlace' => '#fdf5e6', + 'olivedrab' => '#6b8e23', + 'orangered' => '#ff4500', + 'palegoldenrod' => '#eee8aa', + 'paleturquoise' => '#afeeee', + 'papayawhip' => '#ffefd5', + 'peru' => '#cd853f', + 'plum' => '#dda0dd', + 'rosybrown' => '#bc8f8f', + 'saddlebrown' => '#8b4513', + 'sandybrown' => '#f4a460', + 'seashell' => '#fff5ee', + 'slateblue' => '#6a5acd', + 'slategrey' => '#708090', + 'snow' => '#fffafa', + 'tomato' => '#ff6347', + 'violet' => '#ee82ee', + 'wheat' => '#f5deb3', + 'whitesmoke' => '#f5f5f5', + 'yellowgreen' => '#9acd32', + ); +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Surface/CPdf.php b/vendor/dompdf/php-svg-lib/src/Svg/Surface/CPdf.php new file mode 100644 index 0000000..ce807a2 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Surface/CPdf.php @@ -0,0 +1,6411 @@ + "Normal", "opacity" => 1.0]; + + /** + * array Current fill transparency (partial graphics state) + */ + public $currentFillTransparency = ["mode" => "Normal", "opacity" => 1.0]; + + /** + * @var array An array which is used to save the state of the document, mainly the colors and styles + * it is used to temporarily change to another state, then change back to what it was before + */ + public $stateStack = []; + + /** + * @var integer Number of elements within the state stack + */ + public $nStateStack = 0; + + /** + * @var integer Number of page objects within the document + */ + public $numPages = 0; + + /** + * @var array Object Id storage stack + */ + public $stack = []; + + /** + * @var integer Number of elements within the object Id storage stack + */ + public $nStack = 0; + + /** + * an array which contains information about the objects which are not firmly attached to pages + * these have been added with the addObject function + */ + public $looseObjects = []; + + /** + * array contains information about how the loose objects are to be added to the document + */ + public $addLooseObjects = []; + + /** + * @var integer The objectId of the information object for the document + * this contains authorship, title etc. + */ + public $infoObject = 0; + + /** + * @var integer Number of images being tracked within the document + */ + public $numImages = 0; + + /** + * @var array An array containing options about the document + * it defaults to turning on the compression of the objects + */ + public $options = ['compression' => true]; + + /** + * @var integer The objectId of the first page of the document + */ + public $firstPageId; + + /** + * @var integer The object Id of the procset object + */ + public $procsetObjectId; + + /** + * @var array Store the information about the relationship between font families + * this used so that the code knows which font is the bold version of another font, etc. + * the value of this array is initialised in the constructor function. + */ + public $fontFamilies = []; + + /** + * @var string Folder for php serialized formats of font metrics files. + * If empty string, use same folder as original metrics files. + * This can be passed in from class creator. + * If this folder does not exist or is not writable, Cpdf will be **much** slower. + * Because of potential trouble with php safe mode, folder cannot be created at runtime. + */ + public $fontcache = ''; + + /** + * @var integer The version of the font metrics cache file. + * This value must be manually incremented whenever the internal font data structure is modified. + */ + public $fontcacheVersion = 6; + + /** + * @var string Temporary folder. + * If empty string, will attempt system tmp folder. + * This can be passed in from class creator. + */ + public $tmp = ''; + + /** + * @var string Track if the current font is bolded or italicised + */ + public $currentTextState = ''; + + /** + * @var string Messages are stored here during processing, these can be selected afterwards to give some useful debug information + */ + public $messages = ''; + + /** + * @var string The encryption array for the document encryption is stored here + */ + public $arc4 = ''; + + /** + * @var integer The object Id of the encryption information + */ + public $arc4_objnum = 0; + + /** + * @var string The file identifier, used to uniquely identify a pdf document + */ + public $fileIdentifier = ''; + + /** + * @var boolean A flag to say if a document is to be encrypted or not + */ + public $encrypted = false; + + /** + * @var string The encryption key for the encryption of all the document content (structure is not encrypted) + */ + public $encryptionKey = ''; + + /** + * @var array Array which forms a stack to keep track of nested callback functions + */ + public $callback = []; + + /** + * @var integer The number of callback functions in the callback array + */ + public $nCallback = 0; + + /** + * @var array Store label->id pairs for named destinations, these will be used to replace internal links + * done this way so that destinations can be defined after the location that links to them + */ + public $destinations = []; + + /** + * @var array Store the stack for the transaction commands, each item in here is a record of the values of all the + * publiciables within the class, so that the user can rollback at will (from each 'start' command) + * note that this includes the objects array, so these can be large. + */ + public $checkpoint = ''; + + /** + * @var array Table of Image origin filenames and image labels which were already added with o_image(). + * Allows to merge identical images + */ + public $imagelist = []; + + /** + * @var array Table of already added alpha and plain image files for transparent PNG images. + */ + protected $imageAlphaList = []; + + /** + * @var array List of temporary image files to be deleted after processing. + */ + protected $imageCache = []; + + /** + * @var boolean Whether the text passed in should be treated as Unicode or just local character set. + */ + public $isUnicode = false; + + /** + * @var string the JavaScript code of the document + */ + public $javascript = ''; + + /** + * @var boolean whether the compression is possible + */ + protected $compressionReady = false; + + /** + * @var array Current page size + */ + protected $currentPageSize = ["width" => 0, "height" => 0]; + + /** + * @var array All the chars that will be required in the font subsets + */ + protected $stringSubsets = []; + + /** + * @var string The target internal encoding + */ + protected static $targetEncoding = 'Windows-1252'; + + /** + * @var array + */ + protected $byteRange = array(); + + /** + * @var array The list of the core fonts + */ + protected static $coreFonts = [ + 'courier', + 'courier-bold', + 'courier-oblique', + 'courier-boldoblique', + 'helvetica', + 'helvetica-bold', + 'helvetica-oblique', + 'helvetica-boldoblique', + 'times-roman', + 'times-bold', + 'times-italic', + 'times-bolditalic', + 'symbol', + 'zapfdingbats' + ]; + + /** + * Class constructor + * This will start a new document + * + * @param array $pageSize Array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero. + * @param boolean $isUnicode Whether text will be treated as Unicode or not. + * @param string $fontcache The font cache folder + * @param string $tmp The temporary folder + */ + function __construct($pageSize = [0, 0, 612, 792], $isUnicode = false, $fontcache = '', $tmp = '') + { + $this->isUnicode = $isUnicode; + $this->fontcache = rtrim($fontcache, DIRECTORY_SEPARATOR."/\\"); + $this->tmp = ($tmp !== '' ? $tmp : sys_get_temp_dir()); + $this->newDocument($pageSize); + + $this->compressionReady = function_exists('gzcompress'); + + if (in_array('Windows-1252', mb_list_encodings())) { + self::$targetEncoding = 'Windows-1252'; + } + + // also initialize the font families that are known about already + $this->setFontFamily('init'); + } + + public function __destruct() + { + foreach ($this->imageCache as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } + + /** + * Document object methods (internal use only) + * + * There is about one object method for each type of object in the pdf document + * Each function has the same call list ($id,$action,$options). + * $id = the object ID of the object, or what it is to be if it is being created + * $action = a string specifying the action to be performed, though ALL must support: + * 'new' - create the object with the id $id + * 'out' - produce the output for the pdf object + * $options = optional, a string or array containing the various parameters for the object + * + * These, in conjunction with the output function are the ONLY way for output to be produced + * within the pdf 'file'. + */ + + /** + * Destination object, used to specify the location for the user to jump to, presently on opening + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_destination($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'destination', 'info' => []]; + $tmp = ''; + switch ($options['type']) { + case 'XYZ': + /** @noinspection PhpMissingBreakStatementInspection */ + case 'FitR': + $tmp = ' ' . $options['p3'] . $tmp; + case 'FitH': + case 'FitV': + case 'FitBH': + /** @noinspection PhpMissingBreakStatementInspection */ + case 'FitBV': + $tmp = ' ' . $options['p1'] . ' ' . $options['p2'] . $tmp; + case 'Fit': + case 'FitB': + $tmp = $options['type'] . $tmp; + $this->objects[$id]['info']['string'] = $tmp; + $this->objects[$id]['info']['page'] = $options['page']; + } + break; + + case 'out': + $o = &$this->objects[$id]; + + $tmp = $o['info']; + $res = "\n$id 0 obj\n" . '[' . $tmp['page'] . ' 0 R /' . $tmp['string'] . "]\nendobj"; + + return $res; + } + + return null; + } + + /** + * set the viewer preferences + * + * @param $id + * @param $action + * @param string|array $options + * @return string|null + */ + protected function o_viewerPreferences($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'viewerPreferences', 'info' => []]; + break; + + case 'add': + $o = &$this->objects[$id]; + + foreach ($options as $k => $v) { + switch ($k) { + // Boolean keys + case 'HideToolbar': + case 'HideMenubar': + case 'HideWindowUI': + case 'FitWindow': + case 'CenterWindow': + case 'DisplayDocTitle': + case 'PickTrayByPDFSize': + $o['info'][$k] = (bool)$v; + break; + + // Integer keys + case 'NumCopies': + $o['info'][$k] = (int)$v; + break; + + // Name keys + case 'ViewArea': + case 'ViewClip': + case 'PrintClip': + case 'PrintArea': + $o['info'][$k] = (string)$v; + break; + + // Named with limited valid values + case 'NonFullScreenPageMode': + if (!in_array($v, ['UseNone', 'UseOutlines', 'UseThumbs', 'UseOC'])) { + break; + } + $o['info'][$k] = $v; + break; + + case 'Direction': + if (!in_array($v, ['L2R', 'R2L'])) { + break; + } + $o['info'][$k] = $v; + break; + + case 'PrintScaling': + if (!in_array($v, ['None', 'AppDefault'])) { + break; + } + $o['info'][$k] = $v; + break; + + case 'Duplex': + if (!in_array($v, ['None', 'Simplex', 'DuplexFlipShortEdge', 'DuplexFlipLongEdge'])) { + break; + } + $o['info'][$k] = $v; + break; + + // Integer array + case 'PrintPageRange': + // Cast to integer array + foreach ($v as $vK => $vV) { + $v[$vK] = (int)$vV; + } + $o['info'][$k] = array_values($v); + break; + } + } + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< "; + + foreach ($o['info'] as $k => $v) { + if (is_string($v)) { + $v = '/' . $v; + } elseif (is_int($v)) { + $v = (string) $v; + } elseif (is_bool($v)) { + $v = ($v ? 'true' : 'false'); + } elseif (is_array($v)) { + $v = '[' . implode(' ', $v) . ']'; + } + $res .= "\n/$k $v"; + } + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * define the document catalog, the overall controller for the document + * + * @param $id + * @param $action + * @param string|array $options + * @return string|null + */ + protected function o_catalog($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'catalog', 'info' => []]; + $this->catalogId = $id; + break; + + case 'acroform': + case 'outlines': + case 'pages': + case 'openHere': + case 'names': + $o['info'][$action] = $options; + break; + + case 'viewerPreferences': + if (!isset($o['info']['viewerPreferences'])) { + $this->numObj++; + $this->o_viewerPreferences($this->numObj, 'new'); + $o['info']['viewerPreferences'] = $this->numObj; + } + + $vp = $o['info']['viewerPreferences']; + $this->o_viewerPreferences($vp, 'add', $options); + + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Catalog"; + + foreach ($o['info'] as $k => $v) { + switch ($k) { + case 'outlines': + $res .= "\n/Outlines $v 0 R"; + break; + + case 'pages': + $res .= "\n/Pages $v 0 R"; + break; + + case 'viewerPreferences': + $res .= "\n/ViewerPreferences $v 0 R"; + break; + + case 'openHere': + $res .= "\n/OpenAction $v 0 R"; + break; + + case 'names': + $res .= "\n/Names $v 0 R"; + break; + + case 'acroform': + $res .= "\n/AcroForm $v 0 R"; + break; + } + } + + $res .= " >>\nendobj"; + + return $res; + } + + return null; + } + + /** + * object which is a parent to the pages in the document + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_pages($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'pages', 'info' => []]; + $this->o_catalog($this->catalogId, 'pages', $id); + break; + + case 'page': + if (!is_array($options)) { + // then it will just be the id of the new page + $o['info']['pages'][] = $options; + } else { + // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative + // and pos is either 'before' or 'after', saying where this page will fit. + if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])) { + $i = array_search($options['rid'], $o['info']['pages']); + if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i] == $options['rid']) { + + // then there is a match + // make a space + switch ($options['pos']) { + case 'before': + $k = $i; + break; + + case 'after': + $k = $i + 1; + break; + + default: + $k = -1; + break; + } + + if ($k >= 0) { + for ($j = count($o['info']['pages']) - 1; $j >= $k; $j--) { + $o['info']['pages'][$j + 1] = $o['info']['pages'][$j]; + } + + $o['info']['pages'][$k] = $options['id']; + } + } + } + } + break; + + case 'procset': + $o['info']['procset'] = $options; + break; + + case 'mediaBox': + $o['info']['mediaBox'] = $options; + // which should be an array of 4 numbers + $this->currentPageSize = ['width' => $options[2], 'height' => $options[3]]; + break; + + case 'font': + $o['info']['fonts'][] = ['objNum' => $options['objNum'], 'fontNum' => $options['fontNum']]; + break; + + case 'extGState': + $o['info']['extGStates'][] = ['objNum' => $options['objNum'], 'stateNum' => $options['stateNum']]; + break; + + case 'xObject': + $o['info']['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']]; + break; + + case 'out': + if (count($o['info']['pages'])) { + $res = "\n$id 0 obj\n<< /Type /Pages\n/Kids ["; + foreach ($o['info']['pages'] as $v) { + $res .= "$v 0 R\n"; + } + + $res .= "]\n/Count " . count($this->objects[$id]['info']['pages']); + + if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) || + isset($o['info']['procset']) || + (isset($o['info']['extGStates']) && count($o['info']['extGStates'])) + ) { + $res .= "\n/Resources <<"; + + if (isset($o['info']['procset'])) { + $res .= "\n/ProcSet " . $o['info']['procset'] . " 0 R"; + } + + if (isset($o['info']['fonts']) && count($o['info']['fonts'])) { + $res .= "\n/Font << "; + foreach ($o['info']['fonts'] as $finfo) { + $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + + if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])) { + $res .= "\n/XObject << "; + foreach ($o['info']['xObjects'] as $finfo) { + $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + + if (isset($o['info']['extGStates']) && count($o['info']['extGStates'])) { + $res .= "\n/ExtGState << "; + foreach ($o['info']['extGStates'] as $gstate) { + $res .= "\n/GS" . $gstate['stateNum'] . " " . $gstate['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + + $res .= "\n>>"; + if (isset($o['info']['mediaBox'])) { + $tmp = $o['info']['mediaBox']; + $res .= "\n/MediaBox [" . sprintf( + '%.3F %.3F %.3F %.3F', + $tmp[0], + $tmp[1], + $tmp[2], + $tmp[3] + ) . ']'; + } + } + + $res .= "\n >>\nendobj"; + } else { + $res = "\n$id 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj"; + } + + return $res; + } + + return null; + } + + /** + * define the outlines in the doc, empty for now + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_outlines($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'outlines', 'info' => ['outlines' => []]]; + $this->o_catalog($this->catalogId, 'outlines', $id); + break; + + case 'outline': + $o['info']['outlines'][] = $options; + break; + + case 'out': + if (count($o['info']['outlines'])) { + $res = "\n$id 0 obj\n<< /Type /Outlines /Kids ["; + foreach ($o['info']['outlines'] as $v) { + $res .= "$v 0 R "; + } + + $res .= "] /Count " . count($o['info']['outlines']) . " >>\nendobj"; + } else { + $res = "\n$id 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj"; + } + + return $res; + } + + return null; + } + + /** + * an object to hold the font description + * + * @param $id + * @param $action + * @param string|array $options + * @return string|null + * @throws FontNotFoundException + */ + protected function o_font($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'font', + 'info' => [ + 'name' => $options['name'], + 'fontFileName' => $options['fontFileName'], + 'SubType' => 'Type1', + 'isSubsetting' => $options['isSubsetting'] + ] + ]; + $fontNum = $this->numFonts; + $this->objects[$id]['info']['fontNum'] = $fontNum; + + // deal with the encoding and the differences + if (isset($options['differences'])) { + // then we'll need an encoding dictionary + $this->numObj++; + $this->o_fontEncoding($this->numObj, 'new', $options); + $this->objects[$id]['info']['encodingDictionary'] = $this->numObj; + } else { + if (isset($options['encoding'])) { + // we can specify encoding here + switch ($options['encoding']) { + case 'WinAnsiEncoding': + case 'MacRomanEncoding': + case 'MacExpertEncoding': + $this->objects[$id]['info']['encoding'] = $options['encoding']; + break; + + case 'none': + break; + + default: + $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding'; + break; + } + } else { + $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding'; + } + } + + if ($this->fonts[$options['fontFileName']]['isUnicode']) { + // For Unicode fonts, we need to incorporate font data into + // sub-sections that are linked from the primary font section. + // Look at o_fontGIDtoCID and o_fontDescendentCID functions + // for more information. + // + // All of this code is adapted from the excellent changes made to + // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/) + + $toUnicodeId = ++$this->numObj; + $this->o_toUnicode($toUnicodeId, 'new'); + $this->objects[$id]['info']['toUnicode'] = $toUnicodeId; + + $cidFontId = ++$this->numObj; + $this->o_fontDescendentCID($cidFontId, 'new', $options); + $this->objects[$id]['info']['cidFont'] = $cidFontId; + } + + // also tell the pages node about the new font + $this->o_pages($this->currentNode, 'font', ['fontNum' => $fontNum, 'objNum' => $id]); + break; + + case 'add': + $font_options = $this->processFont($id, $o['info']); + + if ($font_options !== false) { + foreach ($font_options as $k => $v) { + switch ($k) { + case 'BaseFont': + $o['info']['name'] = $v; + break; + case 'FirstChar': + case 'LastChar': + case 'Widths': + case 'FontDescriptor': + case 'SubType': + $this->addMessage('o_font ' . $k . " : " . $v); + $o['info'][$k] = $v; + break; + } + } + + // pass values down to descendent font + if (isset($o['info']['cidFont'])) { + $this->o_fontDescendentCID($o['info']['cidFont'], 'add', $font_options); + } + } + break; + + case 'out': + if ($this->fonts[$this->objects[$id]['info']['fontFileName']]['isUnicode']) { + // For Unicode fonts, we need to incorporate font data into + // sub-sections that are linked from the primary font section. + // Look at o_fontGIDtoCID and o_fontDescendentCID functions + // for more information. + // + // All of this code is adapted from the excellent changes made to + // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/) + + $res = "\n$id 0 obj\n<fonts[$fontFileName])) { + return false; + } + + $font = &$this->fonts[$fontFileName]; + + $fileSuffix = $font['fileSuffix']; + $fileSuffixLower = strtolower($font['fileSuffix']); + $fbfile = "$fontFileName.$fileSuffix"; + $isTtfFont = $fileSuffixLower === 'ttf'; + $isPfbFont = $fileSuffixLower === 'pfb'; + + $this->addMessage('selectFont: checking for - ' . $fbfile); + + if (!$fileSuffix) { + $this->addMessage( + 'selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts' + ); + + return false; + } else { + $adobeFontName = isset($font['PostScriptName']) ? $font['PostScriptName'] : $font['FontName']; + // $fontObj = $this->numObj; + $this->addMessage("selectFont: adding font file - $fbfile - $adobeFontName"); + + // find the array of font widths, and put that into an object. + $firstChar = -1; + $lastChar = 0; + $widths = []; + $cid_widths = []; + + foreach ($font['C'] as $num => $d) { + if (intval($num) > 0 || $num == '0') { + if (!$font['isUnicode']) { + // With Unicode, widths array isn't used + if ($lastChar > 0 && $num > $lastChar + 1) { + for ($i = $lastChar + 1; $i < $num; $i++) { + $widths[] = 0; + } + } + } + + $widths[] = $d; + + if ($font['isUnicode']) { + $cid_widths[$num] = $d; + } + + if ($firstChar == -1) { + $firstChar = $num; + } + + $lastChar = $num; + } + } + + // also need to adjust the widths for the differences array + if (isset($object['differences'])) { + foreach ($object['differences'] as $charNum => $charName) { + if ($charNum > $lastChar) { + if (!$object['isUnicode']) { + // With Unicode, widths array isn't used + for ($i = $lastChar + 1; $i <= $charNum; $i++) { + $widths[] = 0; + } + } + + $lastChar = $charNum; + } + + if (isset($font['C'][$charName])) { + $widths[$charNum - $firstChar] = $font['C'][$charName]; + if ($font['isUnicode']) { + $cid_widths[$charName] = $font['C'][$charName]; + } + } + } + } + + if ($font['isUnicode']) { + $font['CIDWidths'] = $cid_widths; + } + + $this->addMessage('selectFont: FirstChar = ' . $firstChar); + $this->addMessage('selectFont: LastChar = ' . $lastChar); + + $widthid = -1; + + if (!$font['isUnicode']) { + // With Unicode, widths array isn't used + + $this->numObj++; + $this->o_contents($this->numObj, 'new', 'raw'); + $this->objects[$this->numObj]['c'] .= '[' . implode(' ', $widths) . ']'; + $widthid = $this->numObj; + } + + $missing_width = 500; + $stemV = 70; + + if (isset($font['MissingWidth'])) { + $missing_width = $font['MissingWidth']; + } + if (isset($font['StdVW'])) { + $stemV = $font['StdVW']; + } else { + if (isset($font['Weight']) && preg_match('!(bold|black)!i', $font['Weight'])) { + $stemV = 120; + } + } + + // load the pfb file, and put that into an object too. + // note that pdf supports only binary format type 1 font files, though there is a + // simple utility to convert them from pfa to pfb. + $data = file_get_contents($fbfile); + + // create the font descriptor + $this->numObj++; + $fontDescriptorId = $this->numObj; + + $this->numObj++; + $pfbid = $this->numObj; + + // determine flags (more than a little flakey, hopefully will not matter much) + $flags = 0; + + if ($font['ItalicAngle'] != 0) { + $flags += pow(2, 6); + } + + if ($font['IsFixedPitch'] === 'true') { + $flags += 1; + } + + $flags += pow(2, 5); // assume non-sybolic + $list = [ + 'Ascent' => 'Ascender', + 'CapHeight' => 'Ascender', //FIXME: php-font-lib is not grabbing this value, so we'll fake it and use the Ascender value // 'CapHeight' + 'MissingWidth' => 'MissingWidth', + 'Descent' => 'Descender', + 'FontBBox' => 'FontBBox', + 'ItalicAngle' => 'ItalicAngle' + ]; + $fdopt = [ + 'Flags' => $flags, + 'FontName' => $adobeFontName, + 'StemV' => $stemV + ]; + + foreach ($list as $k => $v) { + if (isset($font[$v])) { + $fdopt[$k] = $font[$v]; + } + } + + if ($isPfbFont) { + $fdopt['FontFile'] = $pfbid; + } elseif ($isTtfFont) { + $fdopt['FontFile2'] = $pfbid; + } + + $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt); + + // embed the font program + $this->o_contents($this->numObj, 'new'); + $this->objects[$pfbid]['c'] .= $data; + + // determine the cruicial lengths within this file + if ($isPfbFont) { + $l1 = strpos($data, 'eexec') + 6; + $l2 = strpos($data, '00000000') - $l1; + $l3 = mb_strlen($data, '8bit') - $l2 - $l1; + $this->o_contents( + $this->numObj, + 'add', + ['Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3] + ); + } elseif ($isTtfFont) { + $l1 = mb_strlen($data, '8bit'); + $this->o_contents($this->numObj, 'add', ['Length1' => $l1]); + } + + // tell the font object about all this new stuff + $options = [ + 'BaseFont' => $adobeFontName, + 'MissingWidth' => $missing_width, + 'Widths' => $widthid, + 'FirstChar' => $firstChar, + 'LastChar' => $lastChar, + 'FontDescriptor' => $fontDescriptorId + ]; + + if ($isTtfFont) { + $options['SubType'] = 'TrueType'; + } + + $this->addMessage("adding extra info to font.($fontObjId)"); + + foreach ($options as $fk => $fv) { + $this->addMessage("$fk : $fv"); + } + } + + return $options; + } + + /** + * A toUnicode section, needed for unicode fonts + * + * @param $id + * @param $action + * @return null|string + */ + protected function o_toUnicode($id, $action) + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'toUnicode' + ]; + break; + case 'add': + break; + case 'out': + $ordering = 'UCS'; + $registry = 'Adobe'; + + if ($this->encrypted) { + $this->encryptInit($id); + $ordering = $this->ARC4($ordering); + $registry = $this->filterText($this->ARC4($registry), false, false); + } + + $stream = <<> def +/CMapName /Adobe-Identity-UCS def +/CMapType 2 def +1 begincodespacerange +<0000> +endcodespacerange +1 beginbfrange +<0000> <0000> +endbfrange +endcmap +CMapName currentdict /CMap defineresource pop +end +end +EOT; + + $res = "\n$id 0 obj\n"; + $res .= "<>\n"; + $res .= "stream\n" . $stream . "\nendstream" . "\nendobj"; + + return $res; + } + + return null; + } + + /** + * a font descriptor, needed for including additional fonts + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_fontDescriptor($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'fontDescriptor', 'info' => $options]; + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /FontDescriptor\n"; + foreach ($o['info'] as $label => $value) { + switch ($label) { + case 'Ascent': + case 'CapHeight': + case 'Descent': + case 'Flags': + case 'ItalicAngle': + case 'StemV': + case 'AvgWidth': + case 'Leading': + case 'MaxWidth': + case 'MissingWidth': + case 'StemH': + case 'XHeight': + case 'CharSet': + if (mb_strlen($value, '8bit')) { + $res .= "/$label $value\n"; + } + + break; + case 'FontFile': + case 'FontFile2': + case 'FontFile3': + $res .= "/$label $value 0 R\n"; + break; + + case 'FontBBox': + $res .= "/$label [$value[0] $value[1] $value[2] $value[3]]\n"; + break; + + case 'FontName': + $res .= "/$label /$value\n"; + break; + } + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * the font encoding + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_fontEncoding($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + // the options array should contain 'differences' and maybe 'encoding' + $this->objects[$id] = ['t' => 'fontEncoding', 'info' => $options]; + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Encoding\n"; + if (!isset($o['info']['encoding'])) { + $o['info']['encoding'] = 'WinAnsiEncoding'; + } + + if ($o['info']['encoding'] !== 'none') { + $res .= "/BaseEncoding /" . $o['info']['encoding'] . "\n"; + } + + $res .= "/Differences \n["; + + $onum = -100; + + foreach ($o['info']['differences'] as $num => $label) { + if ($num != $onum + 1) { + // we cannot make use of consecutive numbering + $res .= "\n$num /$label"; + } else { + $res .= " /$label"; + } + + $onum = $num; + } + + $res .= "\n]\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * a descendent cid font, needed for unicode fonts + * + * @param $id + * @param $action + * @param string|array $options + * @return null|string + */ + protected function o_fontDescendentCID($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'fontDescendentCID', 'info' => $options]; + + // we need a CID system info section + $cidSystemInfoId = ++$this->numObj; + $this->o_cidSystemInfo($cidSystemInfoId, 'new'); + $this->objects[$id]['info']['cidSystemInfo'] = $cidSystemInfoId; + + // and a CID to GID map + $cidToGidMapId = ++$this->numObj; + $this->o_fontGIDtoCIDMap($cidToGidMapId, 'new', $options); + $this->objects[$id]['info']['cidToGidMap'] = $cidToGidMapId; + break; + + case 'add': + foreach ($options as $k => $v) { + switch ($k) { + case 'BaseFont': + $o['info']['name'] = $v; + break; + + case 'FirstChar': + case 'LastChar': + case 'MissingWidth': + case 'FontDescriptor': + case 'SubType': + $this->addMessage("o_fontDescendentCID $k : $v"); + $o['info'][$k] = $v; + break; + } + } + + // pass values down to cid to gid map + $this->o_fontGIDtoCIDMap($o['info']['cidToGidMap'], 'add', $options); + break; + + case 'out': + $res = "\n$id 0 obj\n"; + $res .= "<fonts[$o['info']['fontFileName']]['CIDWidths'])) { + $cid_widths = &$this->fonts[$o['info']['fontFileName']]['CIDWidths']; + $w = ''; + foreach ($cid_widths as $cid => $width) { + $w .= "$cid [$width] "; + } + $res .= "/W [$w]\n"; + } + + $res .= "/CIDToGIDMap " . $o['info']['cidToGidMap'] . " 0 R\n"; + $res .= ">>\n"; + $res .= "endobj"; + + return $res; + } + + return null; + } + + /** + * CID system info section, needed for unicode fonts + * + * @param $id + * @param $action + * @return null|string + */ + protected function o_cidSystemInfo($id, $action) + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'cidSystemInfo' + ]; + break; + case 'add': + break; + case 'out': + $ordering = 'UCS'; + $registry = 'Adobe'; + + if ($this->encrypted) { + $this->encryptInit($id); + $ordering = $this->ARC4($ordering); + $registry = $this->ARC4($registry); + } + + + $res = "\n$id 0 obj\n"; + + $res .= '<objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'fontGIDtoCIDMap', 'info' => $options]; + break; + + case 'out': + $res = "\n$id 0 obj\n"; + $fontFileName = $o['info']['fontFileName']; + $tmp = $this->fonts[$fontFileName]['CIDtoGID'] = base64_decode($this->fonts[$fontFileName]['CIDtoGID']); + + $compressed = isset($this->fonts[$fontFileName]['CIDtoGID_Compressed']) && + $this->fonts[$fontFileName]['CIDtoGID_Compressed']; + + if (!$compressed && isset($o['raw'])) { + $res .= $tmp; + } else { + $res .= "<<"; + + if (!$compressed && $this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on this content stream + $compressed = true; + $tmp = gzcompress($tmp, 6); + } + if ($compressed) { + $res .= "\n/Filter /FlateDecode"; + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream"; + } + + $res .= "\nendobj"; + + return $res; + } + + return null; + } + + /** + * the document procset, solves some problems with printing to old PS printers + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_procset($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'procset', 'info' => ['PDF' => 1, 'Text' => 1]]; + $this->o_pages($this->currentNode, 'procset', $id); + $this->procsetObjectId = $id; + break; + + case 'add': + // this is to add new items to the procset list, despite the fact that this is considered + // obsolete, the items are required for printing to some postscript printers + switch ($options) { + case 'ImageB': + case 'ImageC': + case 'ImageI': + $o['info'][$options] = 1; + break; + } + break; + + case 'out': + $res = "\n$id 0 obj\n["; + foreach ($o['info'] as $label => $val) { + $res .= "/$label "; + } + $res .= "]\nendobj"; + + return $res; + } + + return null; + } + + /** + * define the document information + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_info($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->infoObject = $id; + $date = 'D:' . @date('Ymd'); + $this->objects[$id] = [ + 't' => 'info', + 'info' => [ + 'Producer' => 'CPDF (dompdf)', + 'CreationDate' => $date + ] + ]; + break; + case 'Title': + case 'Author': + case 'Subject': + case 'Keywords': + case 'Creator': + case 'Producer': + case 'CreationDate': + case 'ModDate': + case 'Trapped': + $this->objects[$id]['info'][$action] = $options; + break; + + case 'out': + $encrypted = $this->encrypted; + if ($encrypted) { + $this->encryptInit($id); + } + + $res = "\n$id 0 obj\n<<\n"; + $o = &$this->objects[$id]; + foreach ($o['info'] as $k => $v) { + $res .= "/$k ("; + + // dates must be outputted as-is, without Unicode transformations + if ($k !== 'CreationDate' && $k !== 'ModDate') { + $v = $this->utf8toUtf16BE($v); + } + + if ($encrypted) { + $v = $this->ARC4($v); + } + + $res .= $this->filterText($v, false, false); + $res .= ")\n"; + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * an action object, used to link to URLS initially + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_action($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + if (is_array($options)) { + $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => $options['type']]; + } else { + // then assume a URI action + $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => 'URI']; + } + break; + + case 'out': + if ($this->encrypted) { + $this->encryptInit($id); + } + + $res = "\n$id 0 obj\n<< /Type /Action"; + switch ($o['type']) { + case 'ilink': + if (!isset($this->destinations[(string)$o['info']['label']])) { + break; + } + + // there will be an 'label' setting, this is the name of the destination + $res .= "\n/S /GoTo\n/D " . $this->destinations[(string)$o['info']['label']] . " 0 R"; + break; + + case 'URI': + $res .= "\n/S /URI\n/URI ("; + if ($this->encrypted) { + $res .= $this->filterText($this->ARC4($o['info']), false, false); + } else { + $res .= $this->filterText($o['info'], false, false); + } + + $res .= ")"; + break; + } + + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * an annotation object, this will add an annotation to the current page. + * initially will support just link annotations + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_annotation($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + // add the annotation to the current page + $pageId = $this->currentPage; + $this->o_page($pageId, 'annot', $id); + + // and add the action object which is going to be required + switch ($options['type']) { + case 'link': + $this->objects[$id] = ['t' => 'annotation', 'info' => $options]; + $this->numObj++; + $this->o_action($this->numObj, 'new', $options['url']); + $this->objects[$id]['info']['actionId'] = $this->numObj; + break; + + case 'ilink': + // this is to a named internal link + $label = $options['label']; + $this->objects[$id] = ['t' => 'annotation', 'info' => $options]; + $this->numObj++; + $this->o_action($this->numObj, 'new', ['type' => 'ilink', 'label' => $label]); + $this->objects[$id]['info']['actionId'] = $this->numObj; + break; + } + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Annot"; + switch ($o['info']['type']) { + case 'link': + case 'ilink': + $res .= "\n/Subtype /Link"; + break; + } + $res .= "\n/A " . $o['info']['actionId'] . " 0 R"; + $res .= "\n/Border [0 0 0]"; + $res .= "\n/H /I"; + $res .= "\n/Rect [ "; + + foreach ($o['info']['rect'] as $v) { + $res .= sprintf("%.4F ", $v); + } + + $res .= "]"; + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * a page object, it also creates a contents object to hold its contents + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_page($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->numPages++; + $this->objects[$id] = [ + 't' => 'page', + 'info' => [ + 'parent' => $this->currentNode, + 'pageNum' => $this->numPages, + 'mediaBox' => $this->objects[$this->currentNode]['info']['mediaBox'] + ] + ]; + + if (is_array($options)) { + // then this must be a page insertion, array should contain 'rid','pos'=[before|after] + $options['id'] = $id; + $this->o_pages($this->currentNode, 'page', $options); + } else { + $this->o_pages($this->currentNode, 'page', $id); + } + + $this->currentPage = $id; + //make a contents object to go with this page + $this->numObj++; + $this->o_contents($this->numObj, 'new', $id); + $this->currentContents = $this->numObj; + $this->objects[$id]['info']['contents'] = []; + $this->objects[$id]['info']['contents'][] = $this->numObj; + + $match = ($this->numPages % 2 ? 'odd' : 'even'); + foreach ($this->addLooseObjects as $oId => $target) { + if ($target === 'all' || $match === $target) { + $this->objects[$id]['info']['contents'][] = $oId; + } + } + break; + + case 'content': + $o['info']['contents'][] = $options; + break; + + case 'annot': + // add an annotation to this page + if (!isset($o['info']['annot'])) { + $o['info']['annot'] = []; + } + + // $options should contain the id of the annotation dictionary + $o['info']['annot'][] = $options; + break; + + case 'out': + $res = "\n$id 0 obj\n<< /Type /Page"; + if (isset($o['info']['mediaBox'])) { + $tmp = $o['info']['mediaBox']; + $res .= "\n/MediaBox [" . sprintf( + '%.3F %.3F %.3F %.3F', + $tmp[0], + $tmp[1], + $tmp[2], + $tmp[3] + ) . ']'; + } + $res .= "\n/Parent " . $o['info']['parent'] . " 0 R"; + + if (isset($o['info']['annot'])) { + $res .= "\n/Annots ["; + foreach ($o['info']['annot'] as $aId) { + $res .= " $aId 0 R"; + } + $res .= " ]"; + } + + $count = count($o['info']['contents']); + if ($count == 1) { + $res .= "\n/Contents " . $o['info']['contents'][0] . " 0 R"; + } else { + if ($count > 1) { + $res .= "\n/Contents [\n"; + + // reverse the page contents so added objects are below normal content + //foreach (array_reverse($o['info']['contents']) as $cId) { + // Back to normal now that I've got transparency working --Benj + foreach ($o['info']['contents'] as $cId) { + $res .= "$cId 0 R\n"; + } + $res .= "]"; + } + } + + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * the contents objects hold all of the content which appears on pages + * + * @param $id + * @param $action + * @param string|array $options + * @return null|string + */ + protected function o_contents($id, $action, $options = '') + { + if ($action !== 'new') { + $o = &$this->objects[$id]; + } + + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'contents', 'c' => '', 'info' => []]; + if (mb_strlen($options, '8bit') && intval($options)) { + // then this contents is the primary for a page + $this->objects[$id]['onPage'] = $options; + } else { + if ($options === 'raw') { + // then this page contains some other type of system object + $this->objects[$id]['raw'] = 1; + } + } + break; + + case 'add': + // add more options to the declaration + foreach ($options as $k => $v) { + $o['info'][$k] = $v; + } + + case 'out': + $tmp = $o['c']; + $res = "\n$id 0 obj\n"; + + if (isset($this->objects[$id]['raw'])) { + $res .= $tmp; + } else { + $res .= "<<"; + if ($this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on this content stream + $res .= " /Filter /FlateDecode"; + $tmp = gzcompress($tmp, 6); + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + + $res .= "\n/Length " . mb_strlen($tmp, '8bit') . " >>\nstream\n$tmp\nendstream"; + } + + $res .= "\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @return string|null + */ + protected function o_embedjs($id, $action) + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'embedjs', + 'info' => [ + 'Names' => '[(EmbeddedJS) ' . ($id + 1) . ' 0 R]' + ] + ]; + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< "; + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @param string $code + * @return null|string + */ + protected function o_javascript($id, $action, $code = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = [ + 't' => 'javascript', + 'info' => [ + 'S' => '/JavaScript', + 'JS' => '(' . $this->filterText($code, true, false) . ')', + ] + ]; + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< "; + + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + /** + * an image object, will be an XObject in the document, includes description and data + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_image($id, $action, $options = '') + { + switch ($action) { + case 'new': + // make the new object + $this->objects[$id] = ['t' => 'image', 'data' => &$options['data'], 'info' => []]; + + $info =& $this->objects[$id]['info']; + + $info['Type'] = '/XObject'; + $info['Subtype'] = '/Image'; + $info['Width'] = $options['iw']; + $info['Height'] = $options['ih']; + + if (isset($options['masked']) && $options['masked']) { + $info['SMask'] = ($this->numObj - 1) . ' 0 R'; + } + + if (!isset($options['type']) || $options['type'] === 'jpg') { + if (!isset($options['channels'])) { + $options['channels'] = 3; + } + + switch ($options['channels']) { + case 1: + $info['ColorSpace'] = '/DeviceGray'; + break; + case 4: + $info['ColorSpace'] = '/DeviceCMYK'; + break; + default: + $info['ColorSpace'] = '/DeviceRGB'; + break; + } + + if ($info['ColorSpace'] === '/DeviceCMYK') { + $info['Decode'] = '[1 0 1 0 1 0 1 0]'; + } + + $info['Filter'] = '/DCTDecode'; + $info['BitsPerComponent'] = 8; + } else { + if ($options['type'] === 'png') { + $info['Filter'] = '/FlateDecode'; + $info['DecodeParms'] = '<< /Predictor 15 /Colors ' . $options['ncolor'] . ' /Columns ' . $options['iw'] . ' /BitsPerComponent ' . $options['bitsPerComponent'] . '>>'; + + if ($options['isMask']) { + $info['ColorSpace'] = '/DeviceGray'; + } else { + if (mb_strlen($options['pdata'], '8bit')) { + $tmp = ' [ /Indexed /DeviceRGB ' . (mb_strlen($options['pdata'], '8bit') / 3 - 1) . ' '; + $this->numObj++; + $this->o_contents($this->numObj, 'new'); + $this->objects[$this->numObj]['c'] = $options['pdata']; + $tmp .= $this->numObj . ' 0 R'; + $tmp .= ' ]'; + $info['ColorSpace'] = $tmp; + + if (isset($options['transparency'])) { + $transparency = $options['transparency']; + switch ($transparency['type']) { + case 'indexed': + $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] '; + $info['Mask'] = $tmp; + break; + + case 'color-key': + $tmp = ' [ ' . + $transparency['r'] . ' ' . $transparency['r'] . + $transparency['g'] . ' ' . $transparency['g'] . + $transparency['b'] . ' ' . $transparency['b'] . + ' ] '; + $info['Mask'] = $tmp; + break; + } + } + } else { + if (isset($options['transparency'])) { + $transparency = $options['transparency']; + + switch ($transparency['type']) { + case 'indexed': + $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] '; + $info['Mask'] = $tmp; + break; + + case 'color-key': + $tmp = ' [ ' . + $transparency['r'] . ' ' . $transparency['r'] . ' ' . + $transparency['g'] . ' ' . $transparency['g'] . ' ' . + $transparency['b'] . ' ' . $transparency['b'] . + ' ] '; + $info['Mask'] = $tmp; + break; + } + } + $info['ColorSpace'] = '/' . $options['color']; + } + } + + $info['BitsPerComponent'] = $options['bitsPerComponent']; + } + } + + // assign it a place in the named resource dictionary as an external object, according to + // the label passed in with it. + $this->o_pages($this->currentNode, 'xObject', ['label' => $options['label'], 'objNum' => $id]); + + // also make sure that we have the right procset object for it. + $this->o_procset($this->procsetObjectId, 'add', 'ImageC'); + break; + + case 'out': + $o = &$this->objects[$id]; + $tmp = &$o['data']; + $res = "\n$id 0 obj\n<<"; + + foreach ($o['info'] as $k => $v) { + $res .= "\n/$k $v"; + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream\nendobj"; + + return $res; + } + + return null; + } + + /** + * graphics state object + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_extGState($id, $action, $options = "") + { + static $valid_params = [ + "LW", + "LC", + "LC", + "LJ", + "ML", + "D", + "RI", + "OP", + "op", + "OPM", + "Font", + "BG", + "BG2", + "UCR", + "TR", + "TR2", + "HT", + "FL", + "SM", + "SA", + "BM", + "SMask", + "CA", + "ca", + "AIS", + "TK" + ]; + + switch ($action) { + case "new": + $this->objects[$id] = ['t' => 'extGState', 'info' => $options]; + + // Tell the pages about the new resource + $this->numStates++; + $this->o_pages($this->currentNode, 'extGState', ["objNum" => $id, "stateNum" => $this->numStates]); + break; + + case "out": + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< /Type /ExtGState\n"; + + foreach ($o["info"] as $k => $v) { + if (!in_array($k, $valid_params)) { + continue; + } + $res .= "/$k $v\n"; + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param integer $id + * @param string $action + * @param mixed $options + * @return string + */ + protected function o_xobject($id, $action, $options = '') + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'xobject', 'info' => $options, 'c' => '']; + break; + + case 'procset': + $this->objects[$id]['procset'] = $options; + break; + + case 'font': + $this->objects[$id]['fonts'][$options['fontNum']] = [ + 'objNum' => $options['objNum'], + 'fontNum' => $options['fontNum'] + ]; + break; + + case 'xObject': + $this->objects[$id]['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']]; + break; + + case 'out': + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< /Type /XObject\n"; + + foreach ($o["info"] as $k => $v) { + switch ($k) { + case 'Subtype': + $res .= "/Subtype /$v\n"; + break; + case 'bbox': + $res .= "/BBox ["; + foreach ($v as $value) { + $res .= sprintf("%.4F ", $value); + } + $res .= "]\n"; + break; + default: + $res .= "/$k $v\n"; + break; + } + } + $res .= "/Matrix[1.0 0.0 0.0 1.0 0.0 0.0]\n"; + + $res .= "/Resources <<"; + if (isset($o['procset'])) { + $res .= "\n/ProcSet " . $o['procset'] . " 0 R"; + } else { + $res .= "\n/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"; + } + if (isset($o['fonts']) && count($o['fonts'])) { + $res .= "\n/Font << "; + foreach ($o['fonts'] as $finfo) { + $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + if (isset($o['xObjects']) && count($o['xObjects'])) { + $res .= "\n/XObject << "; + foreach ($o['xObjects'] as $finfo) { + $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R"; + } + $res .= "\n>>"; + } + $res .= "\n>>\n"; + + $tmp = $o["c"]; + if ($this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on this content stream + $res .= " /Filter /FlateDecode\n"; + $tmp = gzcompress($tmp, 6); + } + + if ($this->encrypted) { + $this->encryptInit($id); + $tmp = $this->ARC4($tmp); + } + + $res .= "/Length " . mb_strlen($tmp, '8bit') . " >>\n"; + $res .= "stream\n" . $tmp . "\nendstream" . "\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_acroform($id, $action, $options = '') + { + switch ($action) { + case "new": + $this->o_catalog($this->catalogId, 'acroform', $id); + $this->objects[$id] = array('t' => 'acroform', 'info' => $options); + break; + + case 'addfield': + $this->objects[$id]['info']['Fields'][] = $options; + break; + + case 'font': + $this->objects[$id]['fonts'][$options['fontNum']] = [ + 'objNum' => $options['objNum'], + 'fontNum' => $options['fontNum'] + ]; + break; + + case "out": + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<<"; + + foreach ($o["info"] as $k => $v) { + switch ($k) { + case 'Fields': + $res .= " /Fields ["; + foreach ($v as $i) { + $res .= "$i 0 R "; + } + $res .= "]\n"; + break; + default: + $res .= "/$k $v\n"; + } + } + + $res .= "/DR <<\n"; + if (isset($o['fonts']) && count($o['fonts'])) { + $res .= "/Font << \n"; + foreach ($o['fonts'] as $finfo) { + $res .= "/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R\n"; + } + $res .= ">>\n"; + } + $res .= ">>\n"; + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * @param $id + * @param $action + * @param mixed $options + * @return null|string + */ + protected function o_field($id, $action, $options = '') + { + switch ($action) { + case "new": + $this->o_page($options['pageid'], 'annot', $id); + $this->o_acroform($this->acroFormId, 'addfield', $id); + $this->objects[$id] = ['t' => 'field', 'info' => $options]; + break; + + case 'set': + $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options); + break; + + case "out": + $o = &$this->objects[$id]; + $res = "\n$id 0 obj\n<< /Type /Annot /Subtype /Widget \n"; + + $encrypted = $this->encrypted; + if ($encrypted) { + $this->encryptInit($id); + } + + foreach ($o["info"] as $k => $v) { + switch ($k) { + case 'pageid': + $res .= "/P $v 0 R\n"; + break; + case 'value': + if ($encrypted) { + $v = $this->filterText($this->ARC4($v), false, false); + } + $res .= "/V ($v)\n"; + break; + case 'refvalue': + $res .= "/V $v 0 R\n"; + break; + case 'da': + if ($encrypted) { + $v = $this->filterText($this->ARC4($v), false, false); + } + $res .= "/DA ($v)\n"; + break; + case 'options': + $res .= "/Opt [\n"; + foreach ($v as $opt) { + if ($encrypted) { + $opt = $this->filterText($this->ARC4($opt), false, false); + } + $res .= "($opt)\n"; + } + $res .= "]\n"; + break; + case 'rect': + $res .= "/Rect ["; + foreach ($v as $value) { + $res .= sprintf("%.4F ", $value); + } + $res .= "]\n"; + break; + case 'appearance': + $res .= "/AP << "; + foreach ($v as $a => $ref) { + $res .= "/$a $ref 0 R "; + } + $res .= ">>\n"; + break; + case 'T': + if ($encrypted) { + $v = $this->filterText($this->ARC4($v), false, false); + } + $res .= "/T ($v)\n"; + break; + default: + $res .= "/$k $v\n"; + } + + } + + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * + * @param $id + * @param $action + * @param string $options + * @return null|string + */ + protected function o_sig($id, $action, $options = '') + { + $sign_maxlen = $this->signatureMaxLen; + + switch ($action) { + case "new": + $this->objects[$id] = array('t' => 'sig', 'info' => $options); + $this->byteRange[$id] = ['t' => 'sig']; + break; + + case 'byterange': + $o = &$this->objects[$id]; + $content =& $options['content']; + $content_len = strlen($content); + $pos = strpos($content, sprintf("/ByteRange [ %'.010d", $id)); + $len = strlen('/ByteRange [ ********** ********** ********** ********** ]'); + $rangeStartPos = $pos + $len + 1 + 10; // before '<' + $content = substr_replace($content, str_pad(sprintf('/ByteRange [ 0 %u %u %u ]', $rangeStartPos, $rangeStartPos + $sign_maxlen + 2, $content_len - 2 - $sign_maxlen - $rangeStartPos), $len, ' ', STR_PAD_RIGHT), $pos, $len); + + $fuid = uniqid(); + $tmpInput = $this->tmp . "/pkcs7.tmp." . $fuid . '.in'; + $tmpOutput = $this->tmp . "/pkcs7.tmp." . $fuid . '.out'; + + if (file_put_contents($tmpInput, substr($content, 0, $rangeStartPos)) === false) { + throw new \Exception("Unable to write temporary file for signing."); + } + if (file_put_contents($tmpInput, substr($content, $rangeStartPos + 2 + $sign_maxlen), + FILE_APPEND) === false) { + throw new \Exception("Unable to write temporary file for signing."); + } + + if (openssl_pkcs7_sign($tmpInput, $tmpOutput, + $o['info']['SignCert'], + array($o['info']['PrivKey'], $o['info']['Password']), + array(), PKCS7_BINARY | PKCS7_DETACHED) === false) { + throw new \Exception("Failed to prepare signature."); + } + + $signature = file_get_contents($tmpOutput); + + unlink($tmpInput); + unlink($tmpOutput); + + $sign = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13)); + list($head, $signature) = explode("\n\n", $sign); + + $signature = base64_decode(trim($signature)); + + $signature = current(unpack('H*', $signature)); + $signature = str_pad($signature, $sign_maxlen, '0'); + $siglen = strlen($signature); + if (strlen($signature) > $sign_maxlen) { + throw new \Exception("Signature length ($siglen) exceeds the $sign_maxlen limit."); + } + + $content = substr_replace($content, $signature, $rangeStartPos + 1, $sign_maxlen); + break; + + case "out": + $res = "\n$id 0 obj\n<<\n"; + + $encrypted = $this->encrypted; + if ($encrypted) { + $this->encryptInit($id); + } + + $res .= "/ByteRange " .sprintf("[ %'.010d ********** ********** ********** ]\n", $id); + $res .= "/Contents <" . str_pad('', $sign_maxlen, '0') . ">\n"; + $res .= "/Filter/Adobe.PPKLite\n"; //PPKMS \n"; + $res .= "/Type/Sig/SubFilter/adbe.pkcs7.detached \n"; + + $date = "D:" . substr_replace(date('YmdHisO'), '\'', -2, 0) . '\''; + if ($encrypted) { + $date = $this->ARC4($date); + } + + $res .= "/M ($date)\n"; + $res .= "/Prop_Build << /App << /Name /DomPDF >> /Filter << /Name /Adobe.PPKLite >> >>\n"; + + $o = &$this->objects[$id]; + foreach ($o['info'] as $k => $v) { + switch ($k) { + case 'Name': + case 'Location': + case 'Reason': + case 'ContactInfo': + if ($v !== null && $v !== '') { + $res .= "/$k (" . + ($encrypted ? $this->filterText($this->ARC4($v), false, false) : $v) . ") \n"; + } + break; + } + } + $res .= ">>\nendobj"; + + return $res; + } + + return null; + } + + /** + * encryption object. + * + * @param $id + * @param $action + * @param string $options + * @return string|null + */ + protected function o_encryption($id, $action, $options = '') + { + switch ($action) { + case 'new': + // make the new object + $this->objects[$id] = ['t' => 'encryption', 'info' => $options]; + $this->arc4_objnum = $id; + break; + + case 'keys': + // figure out the additional parameters required + $pad = chr(0x28) . chr(0xBF) . chr(0x4E) . chr(0x5E) . chr(0x4E) . chr(0x75) . chr(0x8A) . chr(0x41) + . chr(0x64) . chr(0x00) . chr(0x4E) . chr(0x56) . chr(0xFF) . chr(0xFA) . chr(0x01) . chr(0x08) + . chr(0x2E) . chr(0x2E) . chr(0x00) . chr(0xB6) . chr(0xD0) . chr(0x68) . chr(0x3E) . chr(0x80) + . chr(0x2F) . chr(0x0C) . chr(0xA9) . chr(0xFE) . chr(0x64) . chr(0x53) . chr(0x69) . chr(0x7A); + + $info = $this->objects[$id]['info']; + + $len = mb_strlen($info['owner'], '8bit'); + + if ($len > 32) { + $owner = substr($info['owner'], 0, 32); + } else { + if ($len < 32) { + $owner = $info['owner'] . substr($pad, 0, 32 - $len); + } else { + $owner = $info['owner']; + } + } + + $len = mb_strlen($info['user'], '8bit'); + if ($len > 32) { + $user = substr($info['user'], 0, 32); + } else { + if ($len < 32) { + $user = $info['user'] . substr($pad, 0, 32 - $len); + } else { + $user = $info['user']; + } + } + + $tmp = $this->md5_16($owner); + $okey = substr($tmp, 0, 5); + $this->ARC4_init($okey); + $ovalue = $this->ARC4($user); + $this->objects[$id]['info']['O'] = $ovalue; + + // now make the u value, phew. + $tmp = $this->md5_16( + $user . $ovalue . chr($info['p']) . chr(255) . chr(255) . chr(255) . hex2bin($this->fileIdentifier) + ); + + $ukey = substr($tmp, 0, 5); + $this->ARC4_init($ukey); + $this->encryptionKey = $ukey; + $this->encrypted = true; + $uvalue = $this->ARC4($pad); + $this->objects[$id]['info']['U'] = $uvalue; + // initialize the arc4 array + break; + + case 'out': + $o = &$this->objects[$id]; + + $res = "\n$id 0 obj\n<<"; + $res .= "\n/Filter /Standard"; + $res .= "\n/V 1"; + $res .= "\n/R 2"; + $res .= "\n/O (" . $this->filterText($o['info']['O'], false, false) . ')'; + $res .= "\n/U (" . $this->filterText($o['info']['U'], false, false) . ')'; + // and the p-value needs to be converted to account for the twos-complement approach + $o['info']['p'] = (($o['info']['p'] ^ 255) + 1) * -1; + $res .= "\n/P " . ($o['info']['p']); + $res .= "\n>>\nendobj"; + + return $res; + } + + return null; + } + + protected function o_indirect_references($id, $action, $options = null) + { + switch ($action) { + case 'new': + case 'add': + if ($id === 0) { + $id = ++$this->numObj; + $this->o_catalog($this->catalogId, 'names', $id); + $this->objects[$id] = ['t' => 'indirect_references', 'info' => $options]; + $this->indirectReferenceId = $id; + } else { + $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options); + } + break; + case 'out': + $res = "\n$id 0 obj << "; + + foreach ($this->objects[$id]['info'] as $referenceObjName => $referenceObjId) { + $res .= "/$referenceObjName $referenceObjId 0 R "; + } + + $res .= ">> endobj"; + return $res; + } + + return null; + } + + protected function o_names($id, $action, $options = null) + { + switch ($action) { + case 'new': + case 'add': + if ($id === 0) { + $id = ++$this->numObj; + $this->objects[$id] = ['t' => 'names', 'info' => [$options]]; + $this->o_indirect_references($this->indirectReferenceId, 'add', ['EmbeddedFiles' => $id]); + $this->embeddedFilesId = $id; + } else { + $this->objects[$id]['info'][] = $options; + } + break; + case 'out': + $info = &$this->objects[$id]['info']; + $res = ''; + if (count($info) > 0) { + $res = "\n$id 0 obj << /Names [ "; + + if ($this->encrypted) { + $this->encryptInit($id); + } + + foreach ($info as $entry) { + if ($this->encrypted) { + $filename = $this->ARC4($entry['filename']); + } else { + $filename = $entry['filename']; + } + + $res .= "($filename) " . $entry['dict_reference'] . " 0 R "; + } + + $res .= "] >> endobj"; + } + return $res; + } + + return null; + } + + protected function o_embedded_file_dictionary($id, $action, $options = null) + { + switch ($action) { + case 'new': + $embeddedFileId = ++$this->numObj; + $options['embedded_reference'] = $embeddedFileId; + $this->objects[$id] = ['t' => 'embedded_file_dictionary', 'info' => $options]; + $this->o_embedded_file($embeddedFileId, 'new', $options); + $options['dict_reference'] = $id; + $this->o_names($this->embeddedFilesId, 'add', $options); + break; + case 'out': + $info = &$this->objects[$id]['info']; + $filename = $this->utf8toUtf16BE($info['filename']); + $description = $this->utf8toUtf16BE($info['description']); + + if ($this->encrypted) { + $this->encryptInit($id); + $filename = $this->ARC4($filename); + $description = $this->ARC4($description); + } + + $filename = $this->filterText($filename, false, false); + $description = $this->filterText($description, false, false); + + $res = "\n$id 0 obj <>"; + $res .= " /F ($filename) /UF ($filename) /Desc ($description)"; + $res .= " >> endobj"; + return $res; + } + + return null; + } + + protected function o_embedded_file($id, $action, $options = null): ?string + { + switch ($action) { + case 'new': + $this->objects[$id] = ['t' => 'embedded_file', 'info' => $options]; + break; + case 'out': + $info = &$this->objects[$id]['info']; + + if ($this->compressionReady) { + $filepath = $info['filepath']; + $checksum = md5_file($filepath); + $f = fopen($filepath, "rb"); + + $file_content_compressed = ''; + $deflateContext = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 6]); + while (($block = fread($f, 8192))) { + $file_content_compressed .= deflate_add($deflateContext, $block, ZLIB_NO_FLUSH); + } + $file_content_compressed .= deflate_add($deflateContext, '', ZLIB_FINISH); + $file_size_uncompressed = ftell($f); + fclose($f); + } else { + $file_content = file_get_contents($info['filepath']); + $file_size_uncompressed = mb_strlen($file_content, '8bit'); + $checksum = md5($file_content); + } + + if ($this->encrypted) { + $this->encryptInit($id); + $checksum = $this->ARC4($checksum); + $file_content_compressed = $this->ARC4($file_content_compressed); + } + $file_size_compressed = mb_strlen($file_content_compressed, '8bit'); + + $res = "\n$id 0 obj <>" . + " /Type/EmbeddedFile /Filter/FlateDecode" . + " /Length $file_size_compressed >> stream\n$file_content_compressed\nendstream\nendobj"; + + return $res; + } + + return null; + } + + /** + * ARC4 functions + * A series of function to implement ARC4 encoding in PHP + */ + + /** + * calculate the 16 byte version of the 128 bit md5 digest of the string + * + * @param $string + * @return string + */ + function md5_16($string) + { + $tmp = md5($string); + $out = ''; + for ($i = 0; $i <= 30; $i = $i + 2) { + $out .= chr(hexdec(substr($tmp, $i, 2))); + } + + return $out; + } + + /** + * initialize the encryption for processing a particular object + * + * @param $id + */ + function encryptInit($id) + { + $tmp = $this->encryptionKey; + $hex = dechex($id); + if (mb_strlen($hex, '8bit') < 6) { + $hex = substr('000000', 0, 6 - mb_strlen($hex, '8bit')) . $hex; + } + $tmp .= chr(hexdec(substr($hex, 4, 2))) + . chr(hexdec(substr($hex, 2, 2))) + . chr(hexdec(substr($hex, 0, 2))) + . chr(0) + . chr(0) + ; + $key = $this->md5_16($tmp); + $this->ARC4_init(substr($key, 0, 10)); + } + + /** + * initialize the ARC4 encryption + * + * @param string $key + */ + function ARC4_init($key = '') + { + $this->arc4 = ''; + + // setup the control array + if (mb_strlen($key, '8bit') == 0) { + return; + } + + $k = ''; + while (mb_strlen($k, '8bit') < 256) { + $k .= $key; + } + + $k = substr($k, 0, 256); + for ($i = 0; $i < 256; $i++) { + $this->arc4 .= chr($i); + } + + $j = 0; + + for ($i = 0; $i < 256; $i++) { + $t = $this->arc4[$i]; + $j = ($j + ord($t) + ord($k[$i])) % 256; + $this->arc4[$i] = $this->arc4[$j]; + $this->arc4[$j] = $t; + } + } + + /** + * ARC4 encrypt a text string + * + * @param $text + * @return string + */ + function ARC4($text) + { + $len = mb_strlen($text, '8bit'); + $a = 0; + $b = 0; + $c = $this->arc4; + $out = ''; + for ($i = 0; $i < $len; $i++) { + $a = ($a + 1) % 256; + $t = $c[$a]; + $b = ($b + ord($t)) % 256; + $c[$a] = $c[$b]; + $c[$b] = $t; + $k = ord($c[(ord($c[$a]) + ord($c[$b])) % 256]); + $out .= chr(ord($text[$i]) ^ $k); + } + + return $out; + } + + /** + * functions which can be called to adjust or add to the document + */ + + /** + * add a link in the document to an external URL + * + * @param $url + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + */ + function addLink($url, $x0, $y0, $x1, $y1) + { + $this->numObj++; + $info = ['type' => 'link', 'url' => $url, 'rect' => [$x0, $y0, $x1, $y1]]; + $this->o_annotation($this->numObj, 'new', $info); + } + + /** + * add a link in the document to an internal destination (ie. within the document) + * + * @param $label + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + */ + function addInternalLink($label, $x0, $y0, $x1, $y1) + { + $this->numObj++; + $info = ['type' => 'ilink', 'label' => $label, 'rect' => [$x0, $y0, $x1, $y1]]; + $this->o_annotation($this->numObj, 'new', $info); + } + + /** + * set the encryption of the document + * can be used to turn it on and/or set the passwords which it will have. + * also the functions that the user will have are set here, such as print, modify, add + * + * @param string $userPass + * @param string $ownerPass + * @param array $pc + */ + function setEncryption($userPass = '', $ownerPass = '', $pc = []) + { + $p = bindec("11000000"); + + $options = ['print' => 4, 'modify' => 8, 'copy' => 16, 'add' => 32]; + + foreach ($pc as $k => $v) { + if ($v && isset($options[$k])) { + $p += $options[$k]; + } else { + if (isset($options[$v])) { + $p += $options[$v]; + } + } + } + + // implement encryption on the document + if ($this->arc4_objnum == 0) { + // then the block does not exist already, add it. + $this->numObj++; + if (mb_strlen($ownerPass) == 0) { + $ownerPass = $userPass; + } + + $this->o_encryption($this->numObj, 'new', ['user' => $userPass, 'owner' => $ownerPass, 'p' => $p]); + } + } + + /** + * should be used for internal checks, not implemented as yet + */ + function checkAllHere() + { + } + + /** + * return the pdf stream as a string returned from the function + * + * @param bool $debug + * @return string + */ + function output($debug = false) + { + if ($debug) { + // turn compression off + $this->options['compression'] = false; + } + + if ($this->javascript) { + $this->numObj++; + + $js_id = $this->numObj; + $this->o_embedjs($js_id, 'new'); + $this->o_javascript(++$this->numObj, 'new', $this->javascript); + + $id = $this->catalogId; + + $this->o_indirect_references($this->indirectReferenceId, 'add', ['JavaScript' => $js_id]); + } + + if ($this->fileIdentifier === '') { + $tmp = implode('', $this->objects[$this->infoObject]['info']); + $this->fileIdentifier = md5('DOMPDF' . __FILE__ . $tmp . microtime() . mt_rand()); + } + + if ($this->arc4_objnum) { + $this->o_encryption($this->arc4_objnum, 'keys'); + $this->ARC4_init($this->encryptionKey); + } + + $this->checkAllHere(); + + $xref = []; + $content = '%PDF-' . self::PDF_VERSION; + $pos = mb_strlen($content, '8bit'); + + // pre-process o_font objects before output of all objects + foreach ($this->objects as $k => $v) { + if ($v['t'] === 'font') { + $this->o_font($k, 'add'); + } + } + + foreach ($this->objects as $k => $v) { + $tmp = 'o_' . $v['t']; + $cont = $this->$tmp($k, 'out'); + $content .= $cont; + $xref[] = $pos + 1; //+1 to account for \n at the start of each object + $pos += mb_strlen($cont, '8bit'); + } + + $content .= "\nxref\n0 " . (count($xref) + 1) . "\n0000000000 65535 f \n"; + + foreach ($xref as $p) { + $content .= str_pad($p, 10, "0", STR_PAD_LEFT) . " 00000 n \n"; + } + + $content .= "trailer\n<<\n" . + '/Size ' . (count($xref) + 1) . "\n" . + '/Root 1 0 R' . "\n" . + '/Info ' . $this->infoObject . " 0 R\n" + ; + + // if encryption has been applied to this document then add the marker for this dictionary + if ($this->arc4_objnum > 0) { + $content .= '/Encrypt ' . $this->arc4_objnum . " 0 R\n"; + } + + $content .= '/ID[<' . $this->fileIdentifier . '><' . $this->fileIdentifier . ">]\n"; + + // account for \n added at start of xref table + $pos++; + + $content .= ">>\nstartxref\n$pos\n%%EOF\n"; + + if (count($this->byteRange) > 0) { + foreach ($this->byteRange as $k => $v) { + $tmp = 'o_' . $v['t']; + $this->$tmp($k, 'byterange', ['content' => &$content]); + } + } + + return $content; + } + + /** + * initialize a new document + * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum + * this function is called automatically by the constructor function + * + * @param array $pageSize + */ + private function newDocument($pageSize = [0, 0, 612, 792]) + { + $this->numObj = 0; + $this->objects = []; + + $this->numObj++; + $this->o_catalog($this->numObj, 'new'); + + $this->numObj++; + $this->o_outlines($this->numObj, 'new'); + + $this->numObj++; + $this->o_pages($this->numObj, 'new'); + + $this->o_pages($this->numObj, 'mediaBox', $pageSize); + $this->currentNode = 3; + + $this->numObj++; + $this->o_procset($this->numObj, 'new'); + + $this->numObj++; + $this->o_info($this->numObj, 'new'); + + $this->numObj++; + $this->o_page($this->numObj, 'new'); + + // need to store the first page id as there is no way to get it to the user during + // startup + $this->firstPageId = $this->currentContents; + } + + /** + * open the font file and return a php structure containing it. + * first check if this one has been done before and saved in a form more suited to php + * note that if a php serialized version does not exist it will try and make one, but will + * require write access to the directory to do it... it is MUCH faster to have these serialized + * files. + * + * @param $font + */ + private function openFont($font) + { + // assume that $font contains the path and file but not the extension + $name = basename($font); + $dir = dirname($font); + + $fontcache = $this->fontcache; + if ($fontcache == '') { + $fontcache = $dir; + } + + //$name filename without folder and extension of font metrics + //$dir folder of font metrics + //$fontcache folder of runtime created php serialized version of font metrics. + // If this is not given, the same folder as the font metrics will be used. + // Storing and reusing serialized versions improves speed much + + $this->addMessage("openFont: $font - $name"); + + if (!$this->isUnicode || in_array(mb_strtolower(basename($name)), self::$coreFonts)) { + $metrics_name = "$name.afm"; + } else { + $metrics_name = "$name.ufm"; + } + + $cache_name = "$metrics_name.json"; + $this->addMessage("metrics: $metrics_name, cache: $cache_name"); + + if (file_exists($fontcache . '/' . $cache_name)) { + $this->addMessage("openFont: json metrics file exists $fontcache/$cache_name"); + $cached_font_info = json_decode(file_get_contents($fontcache . '/' . $cache_name), true); + if (!isset($cached_font_info['_version_']) || $cached_font_info['_version_'] != $this->fontcacheVersion) { + $this->addMessage('openFont: font cache is out of date, regenerating'); + } else { + $this->fonts[$font] = $cached_font_info; + } + } + + if (!isset($this->fonts[$font]) && file_exists("$dir/$metrics_name")) { + // then rebuild the php_.afm file from the .afm file + $this->addMessage("openFont: build php file from $dir/$metrics_name"); + $data = []; + + // 20 => 'space' + $data['codeToName'] = []; + + // Since we're not going to enable Unicode for the core fonts we need to use a font-based + // setting for Unicode support rather than a global setting. + $data['isUnicode'] = (strtolower(substr($metrics_name, -3)) !== 'afm'); + + $cidtogid = ''; + if ($data['isUnicode']) { + $cidtogid = str_pad('', 256 * 256 * 2, "\x00"); + } + + $file = file("$dir/$metrics_name"); + + foreach ($file as $rowA) { + $row = trim($rowA); + $pos = strpos($row, ' '); + + if ($pos) { + // then there must be some keyword + $key = substr($row, 0, $pos); + switch ($key) { + case 'FontName': + case 'FullName': + case 'FamilyName': + case 'PostScriptName': + case 'Weight': + case 'ItalicAngle': + case 'IsFixedPitch': + case 'CharacterSet': + case 'UnderlinePosition': + case 'UnderlineThickness': + case 'Version': + case 'EncodingScheme': + case 'CapHeight': + case 'XHeight': + case 'Ascender': + case 'Descender': + case 'StdHW': + case 'StdVW': + case 'StartCharMetrics': + case 'FontHeightOffset': // OAR - Added so we can offset the height calculation of a Windows font. Otherwise it's too big. + $data[$key] = trim(substr($row, $pos)); + break; + + case 'FontBBox': + $data[$key] = explode(' ', trim(substr($row, $pos))); + break; + + //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ; + case 'C': // Found in AFM files + $bits = explode(';', trim($row)); + $dtmp = ['C' => null, 'N' => null, 'WX' => null, 'B' => []]; + + foreach ($bits as $bit) { + $bits2 = explode(' ', trim($bit)); + if (mb_strlen($bits2[0], '8bit') == 0) { + continue; + } + + if (count($bits2) > 2) { + $dtmp[$bits2[0]] = []; + for ($i = 1; $i < count($bits2); $i++) { + $dtmp[$bits2[0]][] = $bits2[$i]; + } + } else { + if (count($bits2) == 2) { + $dtmp[$bits2[0]] = $bits2[1]; + } + } + } + + $c = (int)$dtmp['C']; + $n = $dtmp['N']; + $width = floatval($dtmp['WX']); + + if ($c >= 0) { + if (!ctype_xdigit($n) || $c != hexdec($n)) { + $data['codeToName'][$c] = $n; + } + $data['C'][$c] = $width; + } elseif (isset($n)) { + $data['C'][$n] = $width; + } + + if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') { + $data['MissingWidth'] = $width; + } + + break; + + // U 827 ; WX 0 ; N squaresubnosp ; G 675 ; + case 'U': // Found in UFM files + if (!$data['isUnicode']) { + break; + } + + $bits = explode(';', trim($row)); + $dtmp = ['G' => null, 'N' => null, 'U' => null, 'WX' => null]; + + foreach ($bits as $bit) { + $bits2 = explode(' ', trim($bit)); + if (mb_strlen($bits2[0], '8bit') === 0) { + continue; + } + + if (count($bits2) > 2) { + $dtmp[$bits2[0]] = []; + for ($i = 1; $i < count($bits2); $i++) { + $dtmp[$bits2[0]][] = $bits2[$i]; + } + } else { + if (count($bits2) == 2) { + $dtmp[$bits2[0]] = $bits2[1]; + } + } + } + + $c = (int)$dtmp['U']; + $n = $dtmp['N']; + $glyph = $dtmp['G']; + $width = floatval($dtmp['WX']); + + if ($c >= 0) { + // Set values in CID to GID map + if ($c >= 0 && $c < 0xFFFF && $glyph) { + $cidtogid[$c * 2] = chr($glyph >> 8); + $cidtogid[$c * 2 + 1] = chr($glyph & 0xFF); + } + + if (!ctype_xdigit($n) || $c != hexdec($n)) { + $data['codeToName'][$c] = $n; + } + $data['C'][$c] = $width; + } elseif (isset($n)) { + $data['C'][$n] = $width; + } + + if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') { + $data['MissingWidth'] = $width; + } + + break; + + case 'KPX': + break; // don't include them as they are not used yet + //KPX Adieresis yacute -40 + /*$bits = explode(' ', trim($row)); + $data['KPX'][$bits[1]][$bits[2]] = $bits[3]; + break;*/ + } + } + } + + if ($this->compressionReady && $this->options['compression']) { + // then implement ZLIB based compression on CIDtoGID string + $data['CIDtoGID_Compressed'] = true; + $cidtogid = gzcompress($cidtogid, 6); + } + $data['CIDtoGID'] = base64_encode($cidtogid); + $data['_version_'] = $this->fontcacheVersion; + $this->fonts[$font] = $data; + + //Because of potential trouble with php safe mode, expect that the folder already exists. + //If not existing, this will hit performance because of missing cached results. + if (is_dir($fontcache) && is_writable($fontcache)) { + file_put_contents("$fontcache/$cache_name", json_encode($data, JSON_PRETTY_PRINT)); + } + $data = null; + } + + if (!isset($this->fonts[$font])) { + $this->addMessage("openFont: no font file found for $font. Do you need to run load_font.php?"); + } + } + + /** + * if the font is not loaded then load it and make the required object + * else just make it the current font + * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding' + * note that encoding='none' will need to be used for symbolic fonts + * and 'differences' => an array of mappings between numbers 0->255 and character names. + * + * @param string $fontName + * @param string $encoding + * @param bool $set + * @param bool $isSubsetting + * @return int + * @throws FontNotFoundException + */ + function selectFont($fontName, $encoding = '', $set = true, $isSubsetting = true) + { + if ($fontName === null || $fontName === '') { + return $this->currentFontNum; + } + + $ext = substr($fontName, -4); + if ($ext === '.afm' || $ext === '.ufm') { + $fontName = substr($fontName, 0, mb_strlen($fontName) - 4); + } + + if (!isset($this->fonts[$fontName])) { + $this->addMessage("selectFont: selecting - $fontName - $encoding, $set"); + + // load the file + $this->openFont($fontName); + + if (isset($this->fonts[$fontName])) { + $this->numObj++; + $this->numFonts++; + + $font = &$this->fonts[$fontName]; + + $name = basename($fontName); + $options = ['name' => $name, 'fontFileName' => $fontName, 'isSubsetting' => $isSubsetting]; + + if (is_array($encoding)) { + // then encoding and differences might be set + if (isset($encoding['encoding'])) { + $options['encoding'] = $encoding['encoding']; + } + + if (isset($encoding['differences'])) { + $options['differences'] = $encoding['differences']; + } + } else { + if (mb_strlen($encoding, '8bit')) { + // then perhaps only the encoding has been set + $options['encoding'] = $encoding; + } + } + + $this->o_font($this->numObj, 'new', $options); + + if (file_exists("$fontName.ttf")) { + $fileSuffix = 'ttf'; + } elseif (file_exists("$fontName.TTF")) { + $fileSuffix = 'TTF'; + } elseif (file_exists("$fontName.pfb")) { + $fileSuffix = 'pfb'; + } elseif (file_exists("$fontName.PFB")) { + $fileSuffix = 'PFB'; + } else { + $fileSuffix = ''; + } + + $font['fileSuffix'] = $fileSuffix; + + $font['fontNum'] = $this->numFonts; + $font['isSubsetting'] = $isSubsetting && $font['isUnicode'] && strtolower($fileSuffix) === 'ttf'; + + // also set the differences here, note that this means that these will take effect only the + //first time that a font is selected, else they are ignored + if (isset($options['differences'])) { + $font['differences'] = $options['differences']; + } + } + } + + if ($set && isset($this->fonts[$fontName])) { + // so if for some reason the font was not set in the last one then it will not be selected + $this->currentBaseFont = $fontName; + + // the next lines mean that if a new font is selected, then the current text state will be + // applied to it as well. + $this->currentFont = $this->currentBaseFont; + $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; + } + + return $this->currentFontNum; + } + + /** + * sets up the current font, based on the font families, and the current text state + * note that this system is quite flexible, a bold-italic font can be completely different to a + * italic-bold font, and even bold-bold will have to be defined within the family to have meaning + * This function is to be called whenever the currentTextState is changed, it will update + * the currentFont setting to whatever the appropriate family one is. + * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont + * This function will change the currentFont to whatever it should be, but will not change the + * currentBaseFont. + */ + private function setCurrentFont() + { + // if (strlen($this->currentBaseFont) == 0){ + // // then assume an initial font + // $this->selectFont($this->defaultFont); + // } + // $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1); + // if (strlen($this->currentTextState) + // && isset($this->fontFamilies[$cf]) + // && isset($this->fontFamilies[$cf][$this->currentTextState])){ + // // then we are in some state or another + // // and this font has a family, and the current setting exists within it + // // select the font, then return it + // $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState]; + // $this->selectFont($nf,'',0); + // $this->currentFont = $nf; + // $this->currentFontNum = $this->fonts[$nf]['fontNum']; + // } else { + // // the this font must not have the right family member for the current state + // // simply assume the base font + $this->currentFont = $this->currentBaseFont; + $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; + // } + } + + /** + * function for the user to find out what the ID is of the first page that was created during + * startup - useful if they wish to add something to it later. + * + * @return int + */ + function getFirstPageId() + { + return $this->firstPageId; + } + + /** + * add content to the currently active object + * + * @param $content + */ + private function addContent($content) + { + $this->objects[$this->currentContents]['c'] .= $content; + } + + /** + * sets the color for fill operations + * + * @param array $color + * @param bool $force + */ + function setColor($color, $force = false) + { + $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null]; + + if (!$force && $this->currentColor == $new_color) { + return; + } + + if (isset($new_color[3])) { + $this->currentColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F k", $this->currentColor)); + } else { + if (isset($new_color[2])) { + $this->currentColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F rg", $this->currentColor)); + } + } + } + + /** + * sets the color for fill operations + * + * @param string $fillRule + */ + function setFillRule($fillRule) + { + if (!in_array($fillRule, ["nonzero", "evenodd"])) { + return; + } + + $this->fillRule = $fillRule; + } + + /** + * sets the color for stroke operations + * + * @param array $color + * @param bool $force + */ + function setStrokeColor($color, $force = false) + { + $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null]; + + if (!$force && $this->currentStrokeColor == $new_color) { + return; + } + + if (isset($new_color[3])) { + $this->currentStrokeColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F K", $this->currentStrokeColor)); + } else { + if (isset($new_color[2])) { + $this->currentStrokeColor = $new_color; + $this->addContent(vsprintf("\n%.3F %.3F %.3F RG", $this->currentStrokeColor)); + } + } + } + + /** + * Set the graphics state for compositions + * + * @param $parameters + */ + function setGraphicsState($parameters) + { + // Create a new graphics state object if necessary + if (($gstate = array_search($parameters, $this->gstates)) === false) { + $this->numObj++; + $this->o_extGState($this->numObj, 'new', $parameters); + $gstate = $this->numStates; + $this->gstates[$gstate] = $parameters; + } + $this->addContent("\n/GS$gstate gs"); + } + + /** + * Set current blend mode & opacity for lines. + * + * Valid blend modes are: + * + * Normal, Multiply, Screen, Overlay, Darken, Lighten, + * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, + * Exclusion + * + * @param string $mode the blend mode to use + * @param float $opacity 0.0 fully transparent, 1.0 fully opaque + */ + function setLineTransparency($mode, $opacity) + { + static $blend_modes = [ + "Normal", + "Multiply", + "Screen", + "Overlay", + "Darken", + "Lighten", + "ColorDogde", + "ColorBurn", + "HardLight", + "SoftLight", + "Difference", + "Exclusion" + ]; + + if (!in_array($mode, $blend_modes)) { + $mode = "Normal"; + } + + if (is_null($this->currentLineTransparency)) { + $this->currentLineTransparency = []; + } + + if ($mode === (key_exists('mode', $this->currentLineTransparency) ? + $this->currentLineTransparency['mode'] : '') && + $opacity === (key_exists('opacity', $this->currentLineTransparency) ? + $this->currentLineTransparency["opacity"] : '')) { + return; + } + + $this->currentLineTransparency["mode"] = $mode; + $this->currentLineTransparency["opacity"] = $opacity; + + $options = [ + "BM" => "/$mode", + "CA" => (float)$opacity + ]; + + $this->setGraphicsState($options); + } + + /** + * Set current blend mode & opacity for filled objects. + * + * Valid blend modes are: + * + * Normal, Multiply, Screen, Overlay, Darken, Lighten, + * ColorDogde, ColorBurn, HardLight, SoftLight, Difference, + * Exclusion + * + * @param string $mode the blend mode to use + * @param float $opacity 0.0 fully transparent, 1.0 fully opaque + */ + function setFillTransparency($mode, $opacity) + { + static $blend_modes = [ + "Normal", + "Multiply", + "Screen", + "Overlay", + "Darken", + "Lighten", + "ColorDogde", + "ColorBurn", + "HardLight", + "SoftLight", + "Difference", + "Exclusion" + ]; + + if (!in_array($mode, $blend_modes)) { + $mode = "Normal"; + } + + if (is_null($this->currentFillTransparency)) { + $this->currentFillTransparency = []; + } + + if ($mode === (key_exists('mode', $this->currentFillTransparency) ? + $this->currentFillTransparency['mode'] : '') && + $opacity === (key_exists('opacity', $this->currentFillTransparency) ? + $this->currentFillTransparency["opacity"] : '')) { + return; + } + + $this->currentFillTransparency["mode"] = $mode; + $this->currentFillTransparency["opacity"] = $opacity; + + $options = [ + "BM" => "/$mode", + "ca" => (float)$opacity, + ]; + + $this->setGraphicsState($options); + } + + /** + * draw a line from one set of coordinates to another + * + * @param float $x1 + * @param float $y1 + * @param float $x2 + * @param float $y2 + * @param bool $stroke + */ + function line($x1, $y1, $x2, $y2, $stroke = true) + { + $this->addContent(sprintf("\n%.3F %.3F m %.3F %.3F l", $x1, $y1, $x2, $y2)); + + if ($stroke) { + $this->addContent(' S'); + } + } + + /** + * draw a bezier curve based on 4 control points + * + * @param float $x0 + * @param float $y0 + * @param float $x1 + * @param float $y1 + * @param float $x2 + * @param float $y2 + * @param float $x3 + * @param float $y3 + */ + function curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) + { + // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points + // as the control points for the curve. + $this->addContent( + sprintf("\n%.3F %.3F m %.3F %.3F %.3F %.3F %.3F %.3F c S", $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) + ); + } + + /** + * draw a part of an ellipse + * + * @param float $x0 + * @param float $y0 + * @param float $astart + * @param float $afinish + * @param float $r1 + * @param float $r2 + * @param float $angle + * @param int $nSeg + */ + function partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 = 0, $angle = 0, $nSeg = 8) + { + $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, false); + } + + /** + * draw a filled ellipse + * + * @param float $x0 + * @param float $y0 + * @param float $r1 + * @param float $r2 + * @param float $angle + * @param int $nSeg + * @param float $astart + * @param float $afinish + */ + function filledEllipse($x0, $y0, $r1, $r2 = 0, $angle = 0, $nSeg = 8, $astart = 0, $afinish = 360) + { + $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, true, true); + } + + /** + * @param float $x + * @param float $y + */ + function lineTo($x, $y) + { + $this->addContent(sprintf("\n%.3F %.3F l", $x, $y)); + } + + /** + * @param float $x + * @param float $y + */ + function moveTo($x, $y) + { + $this->addContent(sprintf("\n%.3F %.3F m", $x, $y)); + } + + /** + * draw a bezier curve based on 4 control points + * + * @param float $x1 + * @param float $y1 + * @param float $x2 + * @param float $y2 + * @param float $x3 + * @param float $y3 + */ + function curveTo($x1, $y1, $x2, $y2, $x3, $y3) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F %.3F %.3F c", $x1, $y1, $x2, $y2, $x3, $y3)); + } + + /** + * draw a bezier curve based on 4 control points + * + * @param float $cpx + * @param float $cpy + * @param float $x + * @param float $y + */ + function quadTo($cpx, $cpy, $x, $y) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F v", $cpx, $cpy, $x, $y)); + } + + function closePath() + { + $this->addContent(' h'); + } + + function endPath() + { + $this->addContent(' n'); + } + + /** + * draw an ellipse + * note that the part and filled ellipse are just special cases of this function + * + * draws an ellipse in the current line style + * centered at $x0,$y0, radii $r1,$r2 + * if $r2 is not set, then a circle is drawn + * from $astart to $afinish, measured in degrees, running anti-clockwise from the right hand side of the ellipse. + * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a + * pretty crappy shape at 2, as we are approximating with bezier curves. + * + * @param float $x0 + * @param float $y0 + * @param float $r1 + * @param float $r2 + * @param float $angle + * @param int $nSeg + * @param float $astart + * @param float $afinish + * @param bool $close + * @param bool $fill + * @param bool $stroke + * @param bool $incomplete + */ + function ellipse( + $x0, + $y0, + $r1, + $r2 = 0, + $angle = 0, + $nSeg = 8, + $astart = 0, + $afinish = 360, + $close = true, + $fill = false, + $stroke = true, + $incomplete = false + ) { + if ($r1 == 0) { + return; + } + + if ($r2 == 0) { + $r2 = $r1; + } + + if ($nSeg < 2) { + $nSeg = 2; + } + + $astart = deg2rad((float)$astart); + $afinish = deg2rad((float)$afinish); + $totalAngle = $afinish - $astart; + + $dt = $totalAngle / $nSeg; + $dtm = $dt / 3; + + if ($angle != 0) { + $a = -1 * deg2rad((float)$angle); + + $this->addContent( + sprintf("\n q %.3F %.3F %.3F %.3F %.3F %.3F cm", cos($a), -sin($a), sin($a), cos($a), $x0, $y0) + ); + + $x0 = 0; + $y0 = 0; + } + + $t1 = $astart; + $a0 = $x0 + $r1 * cos($t1); + $b0 = $y0 + $r2 * sin($t1); + $c0 = -$r1 * sin($t1); + $d0 = $r2 * cos($t1); + + if (!$incomplete) { + $this->addContent(sprintf("\n%.3F %.3F m ", $a0, $b0)); + } + + for ($i = 1; $i <= $nSeg; $i++) { + // draw this bit of the total curve + $t1 = $i * $dt + $astart; + $a1 = $x0 + $r1 * cos($t1); + $b1 = $y0 + $r2 * sin($t1); + $c1 = -$r1 * sin($t1); + $d1 = $r2 * cos($t1); + + $this->addContent( + sprintf( + "\n%.3F %.3F %.3F %.3F %.3F %.3F c", + ($a0 + $c0 * $dtm), + ($b0 + $d0 * $dtm), + ($a1 - $c1 * $dtm), + ($b1 - $d1 * $dtm), + $a1, + $b1 + ) + ); + + $a0 = $a1; + $b0 = $b1; + $c0 = $c1; + $d0 = $d1; + } + + if (!$incomplete) { + if ($fill) { + $this->addContent(' f'); + } + + if ($stroke) { + if ($close) { + $this->addContent(' s'); // small 's' signifies closing the path as well + } else { + $this->addContent(' S'); + } + } + } + + if ($angle != 0) { + $this->addContent(' Q'); + } + } + + /** + * this sets the line drawing style. + * width, is the thickness of the line in user units + * cap is the type of cap to put on the line, values can be 'butt','round','square' + * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the + * end of the line. + * join can be 'miter', 'round', 'bevel' + * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the + * on and off dashes. + * (2) represents 2 on, 2 off, 2 on , 2 off ... + * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc + * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts. + * + * @param float $width + * @param string $cap + * @param string $join + * @param array $dash + * @param int $phase + */ + function setLineStyle($width = 1, $cap = '', $join = '', $dash = '', $phase = 0) + { + // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day + $string = ''; + + if ($width > 0) { + $string .= "$width w"; + } + + $ca = ['butt' => 0, 'round' => 1, 'square' => 2]; + + if (isset($ca[$cap])) { + $string .= " $ca[$cap] J"; + } + + $ja = ['miter' => 0, 'round' => 1, 'bevel' => 2]; + + if (isset($ja[$join])) { + $string .= " $ja[$join] j"; + } + + if (is_array($dash)) { + $string .= ' [ ' . implode(' ', $dash) . " ] $phase d"; + } + + $this->currentLineStyle = $string; + $this->addContent("\n$string"); + } + + /** + * draw a polygon, the syntax for this is similar to the GD polygon command + * + * @param float[] $p + * @param bool $fill + */ + public function polygon(array $p, bool $fill = false): void + { + $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1])); + + $n = count($p); + for ($i = 2; $i < $n; $i = $i + 2) { + $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i + 1])); + } + + if ($fill) { + $this->addContent(' f'); + } else { + $this->addContent(' S'); + } + } + + /** + * a filled rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not + * the coordinates of the upper-right corner + * + * @param float $x1 + * @param float $y1 + * @param float $width + * @param float $height + */ + function filledRectangle($x1, $y1, $width, $height) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re f", $x1, $y1, $width, $height)); + } + + /** + * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not + * the coordinates of the upper-right corner + * + * @param float $x1 + * @param float $y1 + * @param float $width + * @param float $height + */ + function rectangle($x1, $y1, $width, $height) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re S", $x1, $y1, $width, $height)); + } + + /** + * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not + * the coordinates of the upper-right corner + * + * @param float $x1 + * @param float $y1 + * @param float $width + * @param float $height + */ + function rect($x1, $y1, $width, $height) + { + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re", $x1, $y1, $width, $height)); + } + + function stroke(bool $close = false) + { + $this->addContent("\n" . ($close ? "s" : "S")); + } + + function fill() + { + $this->addContent("\nf" . ($this->fillRule === "evenodd" ? "*" : "")); + } + + function fillStroke(bool $close = false) + { + $this->addContent("\n" . ($close ? "b" : "B") . ($this->fillRule === "evenodd" ? "*" : "")); + } + + /** + * @param string $subtype + * @param integer $x + * @param integer $y + * @param integer $w + * @param integer $h + * @return int + */ + function addXObject($subtype, $x, $y, $w, $h) + { + $id = ++$this->numObj; + $this->o_xobject($id, 'new', ['Subtype' => $subtype, 'bbox' => [$x, $y, $w, $h]]); + return $id; + } + + /** + * @param integer $numXObject + * @param string $type + * @param array $options + */ + function setXObjectResource($numXObject, $type, $options) + { + if (in_array($type, ['procset', 'font', 'xObject'])) { + $this->o_xobject($numXObject, $type, $options); + } + } + + /** + * add signature + * + * $fieldSigId = $cpdf->addFormField(Cpdf::ACROFORM_FIELD_SIG, 'Signature1', 0, 0, 0, 0, 0); + * + * $signatureId = $cpdf->addSignature([ + * 'signcert' => file_get_contents('dompdf.crt'), + * 'privkey' => file_get_contents('dompdf.key'), + * 'password' => 'password', + * 'name' => 'DomPDF DEMO', + * 'location' => 'Home', + * 'reason' => 'First Form', + * 'contactinfo' => 'info' + * ]); + * $cpdf->setFormFieldValue($fieldSigId, "$signatureId 0 R"); + * + * @param string $signcert + * @param string $privkey + * @param string $password + * @param string|null $name + * @param string|null $location + * @param string|null $reason + * @param string|null $contactinfo + * @return int + */ + function addSignature($signcert, $privkey, $password = '', $name = null, $location = null, $reason = null, $contactinfo = null) { + $sigId = ++$this->numObj; + $this->o_sig($sigId, 'new', [ + 'SignCert' => $signcert, + 'PrivKey' => $privkey, + 'Password' => $password, + 'Name' => $name, + 'Location' => $location, + 'Reason' => $reason, + 'ContactInfo' => $contactinfo + ]); + + return $sigId; + } + + /** + * add field to form + * + * @param string $type ACROFORM_FIELD_* + * @param string $name + * @param $x0 + * @param $y0 + * @param $x1 + * @param $y1 + * @param integer $ff Field Flag ACROFORM_FIELD_*_* + * @param float $size + * @param array $color + * @return int + */ + public function addFormField($type, $name, $x0, $y0, $x1, $y1, $ff = 0, $size = 10.0, $color = [0, 0, 0]) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $color = implode(' ', $color) . ' rg'; + + $currentFontNum = $this->currentFontNum; + $font = array_filter( + $this->objects[$this->currentNode]['info']['fonts'], + function ($item) use ($currentFontNum) { return $item['fontNum'] == $currentFontNum; } + ); + + $this->o_acroform($this->acroFormId, 'font', + ['objNum' => $font[0]['objNum'], 'fontNum' => $font[0]['fontNum']]); + + $fieldId = ++$this->numObj; + $this->o_field($fieldId, 'new', [ + 'rect' => [$x0, $y0, $x1, $y1], + 'F' => 4, + 'FT' => "/$type", + 'T' => $name, + 'Ff' => $ff, + 'pageid' => $this->currentPage, + 'da' => "$color /F$this->currentFontNum " . sprintf('%.1F Tf ', $size) + ]); + + return $fieldId; + } + + /** + * set Field value + * + * @param integer $numFieldObj + * @param string $value + */ + public function setFormFieldValue($numFieldObj, $value) + { + $this->o_field($numFieldObj, 'set', ['value' => $value]); + } + + /** + * set Field value (reference) + * + * @param integer $numFieldObj + * @param integer $numObj Object number + */ + public function setFormFieldRefValue($numFieldObj, $numObj) + { + $this->o_field($numFieldObj, 'set', ['refvalue' => $numObj]); + } + + /** + * set Field Appearanc (reference) + * + * @param integer $numFieldObj + * @param integer $normalNumObj + * @param integer|null $rolloverNumObj + * @param integer|null $downNumObj + */ + public function setFormFieldAppearance($numFieldObj, $normalNumObj, $rolloverNumObj = null, $downNumObj = null) + { + $appearance['N'] = $normalNumObj; + + if ($rolloverNumObj !== null) { + $appearance['R'] = $rolloverNumObj; + } + + if ($downNumObj !== null) { + $appearance['D'] = $downNumObj; + } + + $this->o_field($numFieldObj, 'set', ['appearance' => $appearance]); + } + + /** + * set Choice Field option values + * + * @param integer $numFieldObj + * @param array $value + */ + public function setFormFieldOpt($numFieldObj, $value) + { + $this->o_field($numFieldObj, 'set', ['options' => $value]); + } + + /** + * add form to document + * + * @param integer $sigFlags + * @param boolean $needAppearances + */ + public function addForm($sigFlags = 0, $needAppearances = false) + { + $this->acroFormId = ++$this->numObj; + $this->o_acroform($this->acroFormId, 'new', [ + 'NeedAppearances' => $needAppearances ? 'true' : 'false', + 'SigFlags' => $sigFlags + ]); + } + + /** + * save the current graphic state + */ + function save() + { + // we must reset the color cache or it will keep bad colors after clipping + $this->currentColor = null; + $this->currentStrokeColor = null; + $this->addContent("\nq"); + } + + /** + * restore the last graphic state + */ + function restore() + { + // we must reset the color cache or it will keep bad colors after clipping + $this->currentColor = null; + $this->currentStrokeColor = null; + $this->addContent("\nQ"); + } + + /** + * draw a clipping rectangle, all the elements added after this will be clipped + * + * @param float $x1 + * @param float $y1 + * @param float $width + * @param float $height + */ + function clippingRectangle($x1, $y1, $width, $height) + { + $this->save(); + $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re W n", $x1, $y1, $width, $height)); + } + + /** + * draw a clipping rounded rectangle, all the elements added after this will be clipped + * + * @param float $x1 + * @param float $y1 + * @param float $w + * @param float $h + * @param float $rTL + * @param float $rTR + * @param float $rBR + * @param float $rBL + */ + function clippingRectangleRounded($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL) + { + $this->save(); + + // start: top edge, left end + $this->addContent(sprintf("\n%.3F %.3F m ", $x1, $y1 - $rTL + $h)); + + // line: bottom edge, left end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1, $y1 + $rBL)); + + // curve: bottom-left corner + $this->ellipse($x1 + $rBL, $y1 + $rBL, $rBL, 0, 0, 8, 180, 270, false, false, false, true); + + // line: right edge, bottom end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w - $rBR, $y1)); + + // curve: bottom-right corner + $this->ellipse($x1 + $w - $rBR, $y1 + $rBR, $rBR, 0, 0, 8, 270, 360, false, false, false, true); + + // line: right edge, top end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w, $y1 + $h - $rTR)); + + // curve: bottom-right corner + $this->ellipse($x1 + $w - $rTR, $y1 + $h - $rTR, $rTR, 0, 0, 8, 0, 90, false, false, false, true); + + // line: bottom edge, right end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rTL, $y1 + $h)); + + // curve: top-right corner + $this->ellipse($x1 + $rTL, $y1 + $h - $rTL, $rTL, 0, 0, 8, 90, 180, false, false, false, true); + + // line: top edge, left end + $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rBL, $y1)); + + // Close & clip + $this->addContent(" W n"); + } + + /** + * draw a clipping polygon, the syntax for this is similar to the GD polygon command + * + * @param float[] $p + */ + public function clippingPolygon(array $p): void + { + $this->save(); + + $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1])); + + $n = count($p); + for ($i = 2; $i < $n; $i = $i + 2) { + $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i + 1])); + } + + $this->addContent("W n"); + } + + /** + * ends the last clipping shape + */ + function clippingEnd() + { + $this->restore(); + } + + /** + * scale + * + * @param float $s_x scaling factor for width as percent + * @param float $s_y scaling factor for height as percent + * @param float $x Origin abscissa + * @param float $y Origin ordinate + */ + function scale($s_x, $s_y, $x, $y) + { + $y = $this->currentPageSize["height"] - $y; + + $tm = [ + $s_x, + 0, + 0, + $s_y, + $x * (1 - $s_x), + $y * (1 - $s_y) + ]; + + $this->transform($tm); + } + + /** + * translate + * + * @param float $t_x movement to the right + * @param float $t_y movement to the bottom + */ + function translate($t_x, $t_y) + { + $tm = [ + 1, + 0, + 0, + 1, + $t_x, + -$t_y + ]; + + $this->transform($tm); + } + + /** + * rotate + * + * @param float $angle angle in degrees for counter-clockwise rotation + * @param float $x Origin abscissa + * @param float $y Origin ordinate + */ + function rotate($angle, $x, $y) + { + $y = $this->currentPageSize["height"] - $y; + + $a = deg2rad($angle); + $cos_a = cos($a); + $sin_a = sin($a); + + $tm = [ + $cos_a, + -$sin_a, + $sin_a, + $cos_a, + $x - $sin_a * $y - $cos_a * $x, + $y - $cos_a * $y + $sin_a * $x, + ]; + + $this->transform($tm); + } + + /** + * skew + * + * @param float $angle_x + * @param float $angle_y + * @param float $x Origin abscissa + * @param float $y Origin ordinate + */ + function skew($angle_x, $angle_y, $x, $y) + { + $y = $this->currentPageSize["height"] - $y; + + $tan_x = tan(deg2rad($angle_x)); + $tan_y = tan(deg2rad($angle_y)); + + $tm = [ + 1, + -$tan_y, + -$tan_x, + 1, + $tan_x * $y, + $tan_y * $x, + ]; + + $this->transform($tm); + } + + /** + * apply graphic transformations + * + * @param array $tm transformation matrix + */ + function transform($tm) + { + $this->addContent(vsprintf("\n %.3F %.3F %.3F %.3F %.3F %.3F cm", $tm)); + } + + /** + * add a new page to the document + * this also makes the new page the current active object + * + * @param int $insert + * @param int $id + * @param string $pos + * @return int + */ + function newPage($insert = 0, $id = 0, $pos = 'after') + { + // if there is a state saved, then go up the stack closing them + // then on the new page, re-open them with the right setings + + if ($this->nStateStack) { + for ($i = $this->nStateStack; $i >= 1; $i--) { + $this->restoreState($i); + } + } + + $this->numObj++; + + if ($insert) { + // the id from the ezPdf class is the id of the contents of the page, not the page object itself + // query that object to find the parent + $rid = $this->objects[$id]['onPage']; + $opt = ['rid' => $rid, 'pos' => $pos]; + $this->o_page($this->numObj, 'new', $opt); + } else { + $this->o_page($this->numObj, 'new'); + } + + // if there is a stack saved, then put that onto the page + if ($this->nStateStack) { + for ($i = 1; $i <= $this->nStateStack; $i++) { + $this->saveState($i); + } + } + + // and if there has been a stroke or fill color set, then transfer them + if (isset($this->currentColor)) { + $this->setColor($this->currentColor, true); + } + + if (isset($this->currentStrokeColor)) { + $this->setStrokeColor($this->currentStrokeColor, true); + } + + // if there is a line style set, then put this in too + if (mb_strlen($this->currentLineStyle, '8bit')) { + $this->addContent("\n$this->currentLineStyle"); + } + + // the call to the o_page object set currentContents to the present page, so this can be returned as the page id + return $this->currentContents; + } + + /** + * Streams the PDF to the client. + * + * @param string $filename The filename to present to the client. + * @param array $options Associative array: 'compress' => 1 or 0 (default 1); 'Attachment' => 1 or 0 (default 1). + */ + function stream($filename = "document.pdf", $options = []) + { + if (headers_sent()) { + die("Unable to stream pdf: headers already sent"); + } + + if (!isset($options["compress"])) $options["compress"] = true; + if (!isset($options["Attachment"])) $options["Attachment"] = true; + + $debug = !$options['compress']; + $tmp = ltrim($this->output($debug)); + + header("Cache-Control: private"); + header("Content-Type: application/pdf"); + header("Content-Length: " . mb_strlen($tmp, "8bit")); + + $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf"; + $attachment = $options["Attachment"] ? "attachment" : "inline"; + + $encoding = mb_detect_encoding($filename); + $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding); + $fallbackfilename = str_replace("\"", "", $fallbackfilename); + $encodedfilename = rawurlencode($filename); + + $contentDisposition = "Content-Disposition: $attachment; filename=\"$fallbackfilename\""; + if ($fallbackfilename !== $filename) { + $contentDisposition .= "; filename*=UTF-8''$encodedfilename"; + } + header($contentDisposition); + + echo $tmp; + flush(); + } + + /** + * return the height in units of the current font in the given size + * + * @param float $size + * + * @return float + */ + public function getFontHeight(float $size): float + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $font = $this->fonts[$this->currentFont]; + + // for the current font, and the given size, what is the height of the font in user units + if (isset($font['Ascender']) && isset($font['Descender'])) { + $h = $font['Ascender'] - $font['Descender']; + } else { + $h = $font['FontBBox'][3] - $font['FontBBox'][1]; + } + + // have to adjust by a font offset for Windows fonts. unfortunately it looks like + // the bounding box calculations are wrong and I don't know why. + if (isset($font['FontHeightOffset'])) { + // For CourierNew from Windows this needs to be -646 to match the + // Adobe native Courier font. + // + // For FreeMono from GNU this needs to be -337 to match the + // Courier font. + // + // Both have been added manually to the .afm and .ufm files. + $h += (int)$font['FontHeightOffset']; + } + + return $size * $h / 1000; + } + + /** + * @param float $size + * + * @return float + */ + public function getFontXHeight(float $size): float + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $font = $this->fonts[$this->currentFont]; + + // for the current font, and the given size, what is the height of the font in user units + if (isset($font['XHeight'])) { + $xh = $font['Ascender'] - $font['Descender']; + } else { + $xh = $this->getFontHeight($size) / 2; + } + + return $size * $xh / 1000; + } + + /** + * return the font descender, this will normally return a negative number + * if you add this number to the baseline, you get the level of the bottom of the font + * it is in the pdf user units + * + * @param float $size + * + * @return float + */ + public function getFontDescender(float $size): float + { + // note that this will most likely return a negative value + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + //$h = $this->fonts[$this->currentFont]['FontBBox'][1]; + $h = $this->fonts[$this->currentFont]['Descender']; + + return $size * $h / 1000; + } + + /** + * filter the text, this is applied to all text just before being inserted into the pdf document + * it escapes the various things that need to be escaped, and so on + * + * @param $text + * @param bool $bom + * @param bool $convert_encoding + * @return string + */ + function filterText($text, $bom = true, $convert_encoding = true) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + if ($convert_encoding) { + $cf = $this->currentFont; + if (isset($this->fonts[$cf]) && $this->fonts[$cf]['isUnicode']) { + $text = $this->utf8toUtf16BE($text, $bom); + } else { + //$text = html_entity_decode($text, ENT_QUOTES); + $text = mb_convert_encoding($text, self::$targetEncoding, 'UTF-8'); + } + } elseif ($bom) { + $text = $this->utf8toUtf16BE($text, $bom); + } + + // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290) + return strtr($text, [')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r']); + } + + /** + * return array containing codepoints (UTF-8 character values) for the + * string passed in. + * + * based on the excellent TCPDF code by Nicola Asuni and the + * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html + * + * @param string $text UTF-8 string to process + * @return array UTF-8 codepoints array for the string + */ + function utf8toCodePointsArray(&$text) + { + $length = mb_strlen($text, '8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040 + $unicode = []; // array containing unicode values + $bytes = []; // array containing single character byte sequences + $numbytes = 1; // number of octets needed to represent the UTF-8 character + + for ($i = 0; $i < $length; $i++) { + $c = ord($text[$i]); // get one string character at time + if (count($bytes) === 0) { // get starting octect + if ($c <= 0x7F) { + $unicode[] = $c; // use the character "as is" because is ASCII + $numbytes = 1; + } elseif (($c >> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN) + $bytes[] = ($c - 0xC0) << 0x06; + $numbytes = 2; + } elseif (($c >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN) + $bytes[] = ($c - 0xE0) << 0x0C; + $numbytes = 3; + } elseif (($c >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN) + $bytes[] = ($c - 0xF0) << 0x12; + $numbytes = 4; + } else { + // use replacement character for other invalid sequences + $unicode[] = 0xFFFD; + $bytes = []; + $numbytes = 1; + } + } elseif (($c >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN + $bytes[] = $c - 0x80; + if (count($bytes) === $numbytes) { + // compose UTF-8 bytes to a single unicode value + $c = $bytes[0]; + for ($j = 1; $j < $numbytes; $j++) { + $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06)); + } + if ((($c >= 0xD800) and ($c <= 0xDFFF)) or ($c >= 0x10FFFF)) { + // The definition of UTF-8 prohibits encoding character numbers between + // U+D800 and U+DFFF, which are reserved for use with the UTF-16 + // encoding form (as surrogate pairs) and do not directly represent + // characters. + $unicode[] = 0xFFFD; // use replacement character + } else { + $unicode[] = $c; // add char to array + } + // reset data for next char + $bytes = []; + $numbytes = 1; + } + } else { + // use replacement character for other invalid sequences + $unicode[] = 0xFFFD; + $bytes = []; + $numbytes = 1; + } + } + + return $unicode; + } + + /** + * convert UTF-8 to UTF-16 with an additional byte order marker + * at the front if required. + * + * based on the excellent TCPDF code by Nicola Asuni and the + * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html + * + * @param string $text UTF-8 string to process + * @param boolean $bom whether to add the byte order marker + * @return string UTF-16 result string + */ + function utf8toUtf16BE(&$text, $bom = true) + { + $out = $bom ? "\xFE\xFF" : ''; + + $unicode = $this->utf8toCodePointsArray($text); + foreach ($unicode as $c) { + if ($c === 0xFFFD) { + $out .= "\xFF\xFD"; // replacement character + } elseif ($c < 0x10000) { + $out .= chr($c >> 0x08) . chr($c & 0xFF); + } else { + $c -= 0x10000; + $w1 = 0xD800 | ($c >> 0x10); + $w2 = 0xDC00 | ($c & 0x3FF); + $out .= chr($w1 >> 0x08) . chr($w1 & 0xFF) . chr($w2 >> 0x08) . chr($w2 & 0xFF); + } + } + + return $out; + } + + /** + * given a start position and information about how text is to be laid out, calculate where + * on the page the text will end + * + * @param $x + * @param $y + * @param $angle + * @param $size + * @param $wa + * @param $text + * @return array + */ + private function getTextPosition($x, $y, $angle, $size, $wa, $text) + { + // given this information return an array containing x and y for the end position as elements 0 and 1 + $w = $this->getTextWidth($size, $text); + + // need to adjust for the number of spaces in this text + $words = explode(' ', $text); + $nspaces = count($words) - 1; + $w += $wa * $nspaces; + $a = deg2rad((float)$angle); + + return [cos($a) * $w + $x, -sin($a) * $w + $y]; + } + + /** + * Callback method used by smallCaps + * + * @param array $matches + * + * @return string + */ + function toUpper($matches) + { + return mb_strtoupper($matches[0]); + } + + function concatMatches($matches) + { + $str = ""; + foreach ($matches as $match) { + $str .= $match[0]; + } + + return $str; + } + + /** + * register text for font subsetting + * + * @param string $font + * @param string $text + */ + function registerText($font, $text) + { + if (!$this->isUnicode || in_array(mb_strtolower(basename($font)), self::$coreFonts)) { + return; + } + + if (!isset($this->stringSubsets[$font])) { + $base_subset = "\u{fffd}\u{fffe}\u{ffff}"; + $this->stringSubsets[$font] = $this->utf8toCodePointsArray($base_subset); + } + + $this->stringSubsets[$font] = array_unique( + array_merge($this->stringSubsets[$font], $this->utf8toCodePointsArray($text)) + ); + } + + /** + * add text to the document, at a specified location, size and angle on the page + * + * @param float $x + * @param float $y + * @param float $size + * @param string $text + * @param float $angle + * @param float $wordSpaceAdjust + * @param float $charSpaceAdjust + * @param bool $smallCaps + */ + function addText($x, $y, $size, $text, $angle = 0, $wordSpaceAdjust = 0, $charSpaceAdjust = 0, $smallCaps = false) + { + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $text = str_replace(["\r", "\n"], "", $text); + + // if ($smallCaps) { + // preg_match_all("/(\P{Ll}+)/u", $text, $matches, PREG_SET_ORDER); + // $lower = $this->concatMatches($matches); + // d($lower); + + // preg_match_all("/(\p{Ll}+)/u", $text, $matches, PREG_SET_ORDER); + // $other = $this->concatMatches($matches); + // d($other); + + // $text = preg_replace_callback("/\p{Ll}/u", array($this, "toUpper"), $text); + // } + + // if there are any open callbacks, then they should be called, to show the start of the line + if ($this->nCallback > 0) { + for ($i = $this->nCallback; $i > 0; $i--) { + // call each function + $info = [ + 'x' => $x, + 'y' => $y, + 'angle' => $angle, + 'status' => 'sol', + 'p' => $this->callback[$i]['p'], + 'nCallback' => $this->callback[$i]['nCallback'], + 'height' => $this->callback[$i]['height'], + 'descender' => $this->callback[$i]['descender'] + ]; + + $func = $this->callback[$i]['f']; + $this->$func($info); + } + } + + if ($angle == 0) { + $this->addContent(sprintf("\nBT %.3F %.3F Td", $x, $y)); + } else { + $a = deg2rad((float)$angle); + $this->addContent( + sprintf("\nBT %.3F %.3F %.3F %.3F %.3F %.3F Tm", cos($a), -sin($a), sin($a), cos($a), $x, $y) + ); + } + + if ($wordSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tw", $wordSpaceAdjust)); + } + + if ($charSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tc", $charSpaceAdjust)); + } + + $len = mb_strlen($text); + $start = 0; + + if ($start < $len) { + $part = $text; // OAR - Don't need this anymore, given that $start always equals zero. substr($text, $start); + $place_text = $this->filterText($part, false); + // modify unicode text so that extra word spacing is manually implemented (bug #) + if ($this->fonts[$this->currentFont]['isUnicode'] && $wordSpaceAdjust != 0) { + $space_scale = 1000 / $size; + $place_text = str_replace("\x00\x20", "\x00\x20)\x00\x20" . (-round($space_scale * $wordSpaceAdjust)) . "\x00\x20(", $place_text); + } + $this->addContent(" /F$this->currentFontNum " . sprintf('%.1F Tf ', $size)); + $this->addContent(" [($place_text)] TJ"); + } + + if ($wordSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tw", 0)); + } + + if ($charSpaceAdjust != 0) { + $this->addContent(sprintf(" %.3F Tc", 0)); + } + + $this->addContent(' ET'); + + // if there are any open callbacks, then they should be called, to show the end of the line + if ($this->nCallback > 0) { + for ($i = $this->nCallback; $i > 0; $i--) { + // call each function + $tmp = $this->getTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text); + $info = [ + 'x' => $tmp[0], + 'y' => $tmp[1], + 'angle' => $angle, + 'status' => 'eol', + 'p' => $this->callback[$i]['p'], + 'nCallback' => $this->callback[$i]['nCallback'], + 'height' => $this->callback[$i]['height'], + 'descender' => $this->callback[$i]['descender'] + ]; + $func = $this->callback[$i]['f']; + $this->$func($info); + } + } + + if ($this->fonts[$this->currentFont]['isSubsetting']) { + $this->registerText($this->currentFont, $text); + } + } + + /** + * calculate how wide a given text string will be on a page, at a given size. + * this can be called externally, but is also used by the other class functions + * + * @param float $size + * @param string $text + * @param float $wordSpacing + * @param float $charSpacing + * + * @return float + */ + public function getTextWidth(float $size, string $text, float $wordSpacing = 0.0, float $charSpacing = 0.0): float + { + static $ord_cache = []; + + // this function should not change any of the settings, though it will need to + // track any directives which change during calculation, so copy them at the start + // and put them back at the end. + $store_currentTextState = $this->currentTextState; + + if (!$this->numFonts) { + $this->selectFont($this->defaultFont); + } + + $text = str_replace(["\r", "\n"], "", $text); + + // hmm, this is where it all starts to get tricky - use the font information to + // calculate the width of each character, add them up and convert to user units + $w = 0; + $cf = $this->currentFont; + $current_font = $this->fonts[$cf]; + $space_scale = 1000 / ($size > 0 ? $size : 1); + + if ($current_font['isUnicode']) { + // for Unicode, use the code points array to calculate width rather + // than just the string itself + $unicode = $this->utf8toCodePointsArray($text); + + foreach ($unicode as $char) { + // check if we have to replace character + if (isset($current_font['differences'][$char])) { + $char = $current_font['differences'][$char]; + } + + if (isset($current_font['C'][$char])) { + $char_width = $current_font['C'][$char]; + + // add the character width + $w += $char_width; + + // add additional padding for space + if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space + $w += $wordSpacing * $space_scale; + } + } + } + + // add additional char spacing + if ($charSpacing != 0) { + $w += $charSpacing * $space_scale * count($unicode); + } + + } else { + // If CPDF is in Unicode mode but the current font does not support Unicode we need to convert the character set to Windows-1252 + if ($this->isUnicode) { + $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8'); + } + + $len = mb_strlen($text, 'Windows-1252'); + + for ($i = 0; $i < $len; $i++) { + $c = $text[$i]; + $char = isset($ord_cache[$c]) ? $ord_cache[$c] : ($ord_cache[$c] = ord($c)); + + // check if we have to replace character + if (isset($current_font['differences'][$char])) { + $char = $current_font['differences'][$char]; + } + + if (isset($current_font['C'][$char])) { + $char_width = $current_font['C'][$char]; + + // add the character width + $w += $char_width; + + // add additional padding for space + if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space + $w += $wordSpacing * $space_scale; + } + } + } + + // add additional char spacing + if ($charSpacing != 0) { + $w += $charSpacing * $space_scale * $len; + } + } + + $this->currentTextState = $store_currentTextState; + $this->setCurrentFont(); + + return $w * $size / 1000; + } + + /** + * this will be called at a new page to return the state to what it was on the + * end of the previous page, before the stack was closed down + * This is to get around not being able to have open 'q' across pages + * + * @param int $pageEnd + */ + function saveState($pageEnd = 0) + { + if ($pageEnd) { + // this will be called at a new page to return the state to what it was on the + // end of the previous page, before the stack was closed down + // This is to get around not being able to have open 'q' across pages + $opt = $this->stateStack[$pageEnd]; + // ok to use this as stack starts numbering at 1 + $this->setColor($opt['col'], true); + $this->setStrokeColor($opt['str'], true); + $this->addContent("\n" . $opt['lin']); + // $this->currentLineStyle = $opt['lin']; + } else { + $this->nStateStack++; + $this->stateStack[$this->nStateStack] = [ + 'col' => $this->currentColor, + 'str' => $this->currentStrokeColor, + 'lin' => $this->currentLineStyle + ]; + } + + $this->save(); + } + + /** + * restore a previously saved state + * + * @param int $pageEnd + */ + function restoreState($pageEnd = 0) + { + if (!$pageEnd) { + $n = $this->nStateStack; + $this->currentColor = $this->stateStack[$n]['col']; + $this->currentStrokeColor = $this->stateStack[$n]['str']; + $this->addContent("\n" . $this->stateStack[$n]['lin']); + $this->currentLineStyle = $this->stateStack[$n]['lin']; + $this->stateStack[$n] = null; + unset($this->stateStack[$n]); + $this->nStateStack--; + } + + $this->restore(); + } + + /** + * make a loose object, the output will go into this object, until it is closed, then will revert to + * the current one. + * this object will not appear until it is included within a page. + * the function will return the object number + * + * @return int + */ + function openObject() + { + $this->nStack++; + $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage]; + // add a new object of the content type, to hold the data flow + $this->numObj++; + $this->o_contents($this->numObj, 'new'); + $this->currentContents = $this->numObj; + $this->looseObjects[$this->numObj] = 1; + + return $this->numObj; + } + + /** + * open an existing object for editing + * + * @param $id + */ + function reopenObject($id) + { + $this->nStack++; + $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage]; + $this->currentContents = $id; + + // also if this object is the primary contents for a page, then set the current page to its parent + if (isset($this->objects[$id]['onPage'])) { + $this->currentPage = $this->objects[$id]['onPage']; + } + } + + /** + * close an object + */ + function closeObject() + { + // close the object, as long as there was one open in the first place, which will be indicated by + // an objectId on the stack. + if ($this->nStack > 0) { + $this->currentContents = $this->stack[$this->nStack]['c']; + $this->currentPage = $this->stack[$this->nStack]['p']; + $this->nStack--; + // easier to probably not worry about removing the old entries, they will be overwritten + // if there are new ones. + } + } + + /** + * stop an object from appearing on pages from this point on + * + * @param $id + */ + function stopObject($id) + { + // if an object has been appearing on pages up to now, then stop it, this page will + // be the last one that could contain it. + if (isset($this->addLooseObjects[$id])) { + $this->addLooseObjects[$id] = ''; + } + } + + /** + * after an object has been created, it wil only show if it has been added, using this function. + * + * @param $id + * @param string $options + */ + function addObject($id, $options = 'add') + { + // add the specified object to the page + if (isset($this->looseObjects[$id]) && $this->currentContents != $id) { + // then it is a valid object, and it is not being added to itself + switch ($options) { + case 'all': + // then this object is to be added to this page (done in the next block) and + // all future new pages. + $this->addLooseObjects[$id] = 'all'; + + case 'add': + if (isset($this->objects[$this->currentContents]['onPage'])) { + // then the destination contents is the primary for the page + // (though this object is actually added to that page) + $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id); + } + break; + + case 'even': + $this->addLooseObjects[$id] = 'even'; + $pageObjectId = $this->objects[$this->currentContents]['onPage']; + if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 0) { + $this->addObject($id); + // hacky huh :) + } + break; + + case 'odd': + $this->addLooseObjects[$id] = 'odd'; + $pageObjectId = $this->objects[$this->currentContents]['onPage']; + if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 1) { + $this->addObject($id); + // hacky huh :) + } + break; + + case 'next': + $this->addLooseObjects[$id] = 'all'; + break; + + case 'nexteven': + $this->addLooseObjects[$id] = 'even'; + break; + + case 'nextodd': + $this->addLooseObjects[$id] = 'odd'; + break; + } + } + } + + /** + * return a storable representation of a specific object + * + * @param $id + * @return string|null + */ + function serializeObject($id) + { + if (array_key_exists($id, $this->objects)) { + return serialize($this->objects[$id]); + } + + return null; + } + + /** + * restore an object from its stored representation. Returns its new object id. + * + * @param $obj + * @return int + */ + function restoreSerializedObject($obj) + { + $obj_id = $this->openObject(); + $this->objects[$obj_id] = unserialize($obj); + $this->closeObject(); + + return $obj_id; + } + + /** + * Embeds a file inside the PDF + * + * @param string $filepath path to the file to store inside the PDF + * @param string $embeddedFilename the filename displayed in the list of embedded files + * @param string $description a description in the list of embedded files + */ + public function addEmbeddedFile(string $filepath, string $embeddedFilename, string $description): void + { + $this->numObj++; + $this->o_embedded_file_dictionary( + $this->numObj, + 'new', + [ + 'filepath' => $filepath, + 'filename' => $embeddedFilename, + 'description' => $description + ] + ); + } + + /** + * Add content to the documents info object + * + * @param string|array $label + * @param string $value + */ + public function addInfo($label, string $value = ""): void + { + // this will only work if the label is one of the valid ones. + // modify this so that arrays can be passed as well. + // if $label is an array then assume that it is key => value pairs + // else assume that they are both scalar, anything else will probably error + if (is_array($label)) { + foreach ($label as $l => $v) { + $this->o_info($this->infoObject, $l, (string) $v); + } + } else { + $this->o_info($this->infoObject, $label, $value); + } + } + + /** + * set the viewer preferences of the document, it is up to the browser to obey these. + * + * @param $label + * @param int $value + */ + function setPreferences($label, $value = 0) + { + // this will only work if the label is one of the valid ones. + if (is_array($label)) { + foreach ($label as $l => $v) { + $this->o_catalog($this->catalogId, 'viewerPreferences', [$l => $v]); + } + } else { + $this->o_catalog($this->catalogId, 'viewerPreferences', [$label => $value]); + } + } + + /** + * extract an integer from a position in a byte stream + * + * @param $data + * @param $pos + * @param $num + * @return int + */ + private function getBytes(&$data, $pos, $num) + { + // return the integer represented by $num bytes from $pos within $data + $ret = 0; + for ($i = 0; $i < $num; $i++) { + $ret *= 256; + $ret += ord($data[$pos + $i]); + } + + return $ret; + } + + /** + * Check if image already added to pdf image directory. + * If yes, need not to create again (pass empty data) + * + * @param string $imgname + * @return bool + */ + function image_iscached($imgname) + { + return isset($this->imagelist[$imgname]); + } + + /** + * add a PNG image into the document, from a GD object + * this should work with remote files + * + * @param \GdImage|resource $img A GD resource + * @param string $file The PNG file + * @param float $x X position + * @param float $y Y position + * @param float $w Width + * @param float $h Height + * @param bool $is_mask true if the image is a mask + * @param bool $mask true if the image is masked + * @throws Exception + */ + function addImagePng(&$img, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null) + { + if (!function_exists("imagepng")) { + throw new \Exception("The PHP GD extension is required, but is not installed."); + } + + //if already cached, need not to read again + if (isset($this->imagelist[$file])) { + $data = null; + } else { + // Example for transparency handling on new image. Retain for current image + // $tIndex = imagecolortransparent($img); + // if ($tIndex > 0) { + // $tColor = imagecolorsforindex($img, $tIndex); + // $new_tIndex = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']); + // imagefill($new_img, 0, 0, $new_tIndex); + // imagecolortransparent($new_img, $new_tIndex); + // } + // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn + //imagealphablending($img, true); + + //default, but explicitely set to ensure pdf compatibility + imagesavealpha($img, false/*!$is_mask && !$mask*/); + + $error = 0; + //DEBUG_IMG_TEMP + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addImagePng ' . $file . ']'; + } + + ob_start(); + @imagepng($img); + $data = ob_get_clean(); + + if ($data == '') { + $error = 1; + $errormsg = 'trouble writing file from GD'; + //DEBUG_IMG_TEMP + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print 'trouble writing file from GD'; + } + } + + if ($error) { + $this->addMessage('PNG error - (' . $file . ') ' . $errormsg); + + return; + } + } //End isset($this->imagelist[$file]) (png Duplicate removal) + + $this->addPngFromBuf($data, $file, $x, $y, $w, $h, $is_mask, $mask); + } + + /** + * @param $file + * @param $x + * @param $y + * @param $w + * @param $h + * @param $byte + */ + protected function addImagePngAlpha($file, $x, $y, $w, $h, $byte) + { + // generate images + $img = @imagecreatefrompng($file); + + if ($img === false) { + return; + } + + // FIXME The pixel transformation doesn't work well with 8bit PNGs + $eight_bit = ($byte & 4) !== 4; + + $wpx = imagesx($img); + $hpx = imagesy($img); + + imagesavealpha($img, false); + + // create temp alpha file + $tempfile_alpha = @tempnam($this->tmp, "cpdf_img_"); + @unlink($tempfile_alpha); + $tempfile_alpha = "$tempfile_alpha.png"; + + // create temp plain file + $tempfile_plain = @tempnam($this->tmp, "cpdf_img_"); + @unlink($tempfile_plain); + $tempfile_plain = "$tempfile_plain.png"; + + $imgalpha = imagecreate($wpx, $hpx); + imagesavealpha($imgalpha, false); + + // generate gray scale palette (0 -> 255) + for ($c = 0; $c < 256; ++$c) { + imagecolorallocate($imgalpha, $c, $c, $c); + } + + // Use PECL gmagick + Graphics Magic to process transparent PNG images + if (extension_loaded("gmagick")) { + $gmagick = new \Gmagick($file); + $gmagick->setimageformat('png'); + + // Get opacity channel (negative of alpha channel) + $alpha_channel_neg = clone $gmagick; + $alpha_channel_neg->separateimagechannel(\Gmagick::CHANNEL_OPACITY); + + // Negate opacity channel + $alpha_channel = new \Gmagick(); + $alpha_channel->newimage($wpx, $hpx, "#FFFFFF", "png"); + $alpha_channel->compositeimage($alpha_channel_neg, \Gmagick::COMPOSITE_DIFFERENCE, 0, 0); + $alpha_channel->separateimagechannel(\Gmagick::CHANNEL_RED); + $alpha_channel->writeimage($tempfile_alpha); + + // Cast to 8bit+palette + $imgalpha_ = @imagecreatefrompng($tempfile_alpha); + imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx); + imagedestroy($imgalpha_); + imagepng($imgalpha, $tempfile_alpha); + + // Make opaque image + $color_channels = new \Gmagick(); + $color_channels->newimage($wpx, $hpx, "#FFFFFF", "png"); + $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYRED, 0, 0); + $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYGREEN, 0, 0); + $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYBLUE, 0, 0); + $color_channels->writeimage($tempfile_plain); + + $imgplain = @imagecreatefrompng($tempfile_plain); + } + // Use PECL imagick + ImageMagic to process transparent PNG images + elseif (extension_loaded("imagick")) { + // Native cloning was added to pecl-imagick in svn commit 263814 + // the first version containing it was 3.0.1RC1 + static $imagickClonable = null; + if ($imagickClonable === null) { + $imagickClonable = true; + if (defined('Imagick::IMAGICK_EXTVER')) { + $imagickVersion = \Imagick::IMAGICK_EXTVER; + } else { + $imagickVersion = '0'; + } + if (version_compare($imagickVersion, '0.0.1', '>=')) { + $imagickClonable = version_compare($imagickVersion, '3.0.1rc1', '>='); + } + } + + $imagick = new \Imagick($file); + $imagick->setFormat('png'); + + // Get opacity channel (negative of alpha channel) + if ($imagick->getImageAlphaChannel()) { + $alpha_channel = $imagickClonable ? clone $imagick : $imagick->clone(); + $alpha_channel->separateImageChannel(\Imagick::CHANNEL_ALPHA); + // Since ImageMagick7 negate invert transparency as default + if (\Imagick::getVersion()['versionNumber'] < 1800) { + $alpha_channel->negateImage(true); + } + $alpha_channel->writeImage($tempfile_alpha); + + // Cast to 8bit+palette + $imgalpha_ = @imagecreatefrompng($tempfile_alpha); + imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx); + imagedestroy($imgalpha_); + imagepng($imgalpha, $tempfile_alpha); + } else { + $tempfile_alpha = null; + } + + // Make opaque image + $color_channels = new \Imagick(); + $color_channels->newImage($wpx, $hpx, "#FFFFFF", "png"); + $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYRED, 0, 0); + $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYGREEN, 0, 0); + $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYBLUE, 0, 0); + $color_channels->writeImage($tempfile_plain); + + $imgplain = @imagecreatefrompng($tempfile_plain); + } else { + // allocated colors cache + $allocated_colors = []; + + // extract alpha channel + for ($xpx = 0; $xpx < $wpx; ++$xpx) { + for ($ypx = 0; $ypx < $hpx; ++$ypx) { + $color = imagecolorat($img, $xpx, $ypx); + $col = imagecolorsforindex($img, $color); + $alpha = $col['alpha']; + + if ($eight_bit) { + // with gamma correction + $gammacorr = 2.2; + $pixel = round(pow((((127 - $alpha) * 255 / 127) / 255), $gammacorr) * 255); + } else { + // without gamma correction + $pixel = (127 - $alpha) * 2; + + $key = $col['red'] . $col['green'] . $col['blue']; + + if (!isset($allocated_colors[$key])) { + $pixel_img = imagecolorallocate($img, $col['red'], $col['green'], $col['blue']); + $allocated_colors[$key] = $pixel_img; + } else { + $pixel_img = $allocated_colors[$key]; + } + + imagesetpixel($img, $xpx, $ypx, $pixel_img); + } + + imagesetpixel($imgalpha, $xpx, $ypx, $pixel); + } + } + + // extract image without alpha channel + $imgplain = imagecreatetruecolor($wpx, $hpx); + imagecopy($imgplain, $img, 0, 0, 0, 0, $wpx, $hpx); + imagedestroy($img); + + imagepng($imgalpha, $tempfile_alpha); + imagepng($imgplain, $tempfile_plain); + } + + $this->imageAlphaList[$file] = [$tempfile_alpha, $tempfile_plain]; + + // embed mask image + if ($tempfile_alpha) { + $this->addImagePng($imgalpha, $tempfile_alpha, $x, $y, $w, $h, true); + imagedestroy($imgalpha); + $this->imageCache[] = $tempfile_alpha; + } + + // embed image, masked with previously embedded mask + $this->addImagePng($imgplain, $tempfile_plain, $x, $y, $w, $h, false, ($tempfile_alpha !== null)); + imagedestroy($imgplain); + $this->imageCache[] = $tempfile_plain; + } + + /** + * add a PNG image into the document, from a file + * this should work with remote files + * + * @param $file + * @param $x + * @param $y + * @param int $w + * @param int $h + * @throws Exception + */ + function addPngFromFile($file, $x, $y, $w = 0, $h = 0) + { + if (!function_exists("imagecreatefrompng")) { + throw new \Exception("The PHP GD extension is required, but is not installed."); + } + + if (isset($this->imageAlphaList[$file])) { + [$alphaFile, $plainFile] = $this->imageAlphaList[$file]; + + if ($alphaFile) { + $img = null; + $this->addImagePng($img, $alphaFile, $x, $y, $w, $h, true); + } + + $img = null; + $this->addImagePng($img, $plainFile, $x, $y, $w, $h, false, ($plainFile !== null)); + return; + } + + //if already cached, need not to read again + if (isset($this->imagelist[$file])) { + $img = null; + } else { + $info = file_get_contents($file, false, null, 24, 5); + $meta = unpack("CbitDepth/CcolorType/CcompressionMethod/CfilterMethod/CinterlaceMethod", $info); + $bit_depth = $meta["bitDepth"]; + $color_type = $meta["colorType"]; + + // http://www.w3.org/TR/PNG/#11IHDR + // 3 => indexed + // 4 => greyscale with alpha + // 6 => fullcolor with alpha + $is_alpha = in_array($color_type, [4, 6]) || ($color_type == 3 && $bit_depth != 4); + + if ($is_alpha) { // exclude grayscale alpha + $this->addImagePngAlpha($file, $x, $y, $w, $h, $color_type); + return; + } + + //png files typically contain an alpha channel. + //pdf file format or class.pdf does not support alpha blending. + //on alpha blended images, more transparent areas have a color near black. + //This appears in the result on not storing the alpha channel. + //Correct would be the box background image or its parent when transparent. + //But this would make the image dependent on the background. + //Therefore create an image with white background and copy in + //A more natural background than black is white. + //Therefore create an empty image with white background and merge the + //image in with alpha blending. + $imgtmp = @imagecreatefrompng($file); + if (!$imgtmp) { + return; + } + $sx = imagesx($imgtmp); + $sy = imagesy($imgtmp); + $img = imagecreatetruecolor($sx, $sy); + imagealphablending($img, true); + + // @todo is it still needed ?? + $ti = imagecolortransparent($imgtmp); + if ($ti >= 0) { + $tc = imagecolorsforindex($imgtmp, $ti); + $ti = imagecolorallocate($img, $tc['red'], $tc['green'], $tc['blue']); + imagefill($img, 0, 0, $ti); + imagecolortransparent($img, $ti); + } else { + imagefill($img, 1, 1, imagecolorallocate($img, 255, 255, 255)); + } + + imagecopy($img, $imgtmp, 0, 0, 0, 0, $sx, $sy); + imagedestroy($imgtmp); + } + $this->addImagePng($img, $file, $x, $y, $w, $h); + + if ($img) { + imagedestroy($img); + } + } + + /** + * add a PNG image into the document, from a memory buffer of the file + * + * @param $data + * @param $file + * @param $x + * @param $y + * @param float $w + * @param float $h + * @param bool $is_mask + * @param null $mask + */ + function addPngFromBuf(&$data, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null) + { + if (isset($this->imagelist[$file])) { + $data = null; + $info['width'] = $this->imagelist[$file]['w']; + $info['height'] = $this->imagelist[$file]['h']; + $label = $this->imagelist[$file]['label']; + } else { + if ($data == null) { + $this->addMessage('addPngFromBuf error - data not present!'); + + return; + } + + $error = 0; + + if (!$error) { + $header = chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10); + + if (mb_substr($data, 0, 8, '8bit') != $header) { + $error = 1; + + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile this file does not have a valid header ' . $file . ']'; + } + + $errormsg = 'this file does not have a valid header'; + } + } + + if (!$error) { + // set pointer + $p = 8; + $len = mb_strlen($data, '8bit'); + + // cycle through the file, identifying chunks + $haveHeader = 0; + $info = []; + $idata = ''; + $pdata = ''; + + while ($p < $len) { + $chunkLen = $this->getBytes($data, $p, 4); + $chunkType = mb_substr($data, $p + 4, 4, '8bit'); + + switch ($chunkType) { + case 'IHDR': + // this is where all the file information comes from + $info['width'] = $this->getBytes($data, $p + 8, 4); + $info['height'] = $this->getBytes($data, $p + 12, 4); + $info['bitDepth'] = ord($data[$p + 16]); + $info['colorType'] = ord($data[$p + 17]); + $info['compressionMethod'] = ord($data[$p + 18]); + $info['filterMethod'] = ord($data[$p + 19]); + $info['interlaceMethod'] = ord($data[$p + 20]); + + //print_r($info); + $haveHeader = 1; + if ($info['compressionMethod'] != 0) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile unsupported compression method ' . $file . ']'; + } + + $errormsg = 'unsupported compression method'; + } + + if ($info['filterMethod'] != 0) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile unsupported filter method ' . $file . ']'; + } + + $errormsg = 'unsupported filter method'; + } + break; + + case 'PLTE': + $pdata .= mb_substr($data, $p + 8, $chunkLen, '8bit'); + break; + + case 'IDAT': + $idata .= mb_substr($data, $p + 8, $chunkLen, '8bit'); + break; + + case 'tRNS': + //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk + //print "tRNS found, color type = ".$info['colorType']."\n"; + $transparency = []; + + switch ($info['colorType']) { + // indexed color, rbg + case 3: + /* corresponding to entries in the plte chunk + Alpha for palette index 0: 1 byte + Alpha for palette index 1: 1 byte + ...etc... + */ + // there will be one entry for each palette entry. up until the last non-opaque entry. + // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent) + $transparency['type'] = 'indexed'; + $trans = 0; + + for ($i = $chunkLen; $i >= 0; $i--) { + if (ord($data[$p + 8 + $i]) == 0) { + $trans = $i; + } + } + + $transparency['data'] = $trans; + break; + + // grayscale + case 0: + /* corresponding to entries in the plte chunk + Gray: 2 bytes, range 0 .. (2^bitdepth)-1 + */ + // $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale + $transparency['type'] = 'indexed'; + $transparency['data'] = ord($data[$p + 8 + 1]); + break; + + // truecolor + case 2: + /* corresponding to entries in the plte chunk + Red: 2 bytes, range 0 .. (2^bitdepth)-1 + Green: 2 bytes, range 0 .. (2^bitdepth)-1 + Blue: 2 bytes, range 0 .. (2^bitdepth)-1 + */ + $transparency['r'] = $this->getBytes($data, $p + 8, 2); + // r from truecolor + $transparency['g'] = $this->getBytes($data, $p + 10, 2); + // g from truecolor + $transparency['b'] = $this->getBytes($data, $p + 12, 2); + // b from truecolor + + $transparency['type'] = 'color-key'; + break; + + //unsupported transparency type + default: + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile unsupported transparency type ' . $file . ']'; + } + break; + } + + // KS End new code + break; + + default: + break; + } + + $p += $chunkLen + 12; + } + + if (!$haveHeader) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile information header is missing ' . $file . ']'; + } + + $errormsg = 'information header is missing'; + } + + if (isset($info['interlaceMethod']) && $info['interlaceMethod']) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile no support for interlaced images in pdf ' . $file . ']'; + } + + $errormsg = 'There appears to be no support for interlaced images in pdf.'; + } + } + + if (!$error && $info['bitDepth'] > 8) { + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile bit depth of 8 or less is supported ' . $file . ']'; + } + + $errormsg = 'only bit depth of 8 or less is supported'; + } + + if (!$error) { + switch ($info['colorType']) { + case 3: + $color = 'DeviceRGB'; + $ncolor = 1; + break; + + case 2: + $color = 'DeviceRGB'; + $ncolor = 3; + break; + + case 0: + $color = 'DeviceGray'; + $ncolor = 1; + break; + + default: + $error = 1; + + //debugpng + if (defined("DEBUGPNG") && DEBUGPNG) { + print '[addPngFromFile alpha channel not supported: ' . $info['colorType'] . ' ' . $file . ']'; + } + + $errormsg = 'transparency alpha channel not supported, transparency only supported for palette images.'; + } + } + + if ($error) { + $this->addMessage('PNG error - (' . $file . ') ' . $errormsg); + + return; + } + + //print_r($info); + // so this image is ok... add it in. + $this->numImages++; + $im = $this->numImages; + $label = "I$im"; + $this->numObj++; + + // $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width'])); + $options = [ + 'label' => $label, + 'data' => $idata, + 'bitsPerComponent' => $info['bitDepth'], + 'pdata' => $pdata, + 'iw' => $info['width'], + 'ih' => $info['height'], + 'type' => 'png', + 'color' => $color, + 'ncolor' => $ncolor, + 'masked' => $mask, + 'isMask' => $is_mask + ]; + + if (isset($transparency)) { + $options['transparency'] = $transparency; + } + + $this->o_image($this->numObj, 'new', $options); + $this->imagelist[$file] = ['label' => $label, 'w' => $info['width'], 'h' => $info['height']]; + } + + if ($is_mask) { + return; + } + + if ($w <= 0 && $h <= 0) { + $w = $info['width']; + $h = $info['height']; + } + + if ($w <= 0) { + $w = $h / $info['height'] * $info['width']; + } + + if ($h <= 0) { + $h = $w * $info['height'] / $info['width']; + } + + $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ", $w, $h, $x, $y, $label)); + } + + /** + * add a JPEG image into the document, from a file + * + * @param $img + * @param $x + * @param $y + * @param int $w + * @param int $h + */ + function addJpegFromFile($img, $x, $y, $w = 0, $h = 0) + { + // attempt to add a jpeg image straight from a file, using no GD commands + // note that this function is unable to operate on a remote file. + + if (!file_exists($img)) { + return; + } + + if ($this->image_iscached($img)) { + $data = null; + $imageWidth = $this->imagelist[$img]['w']; + $imageHeight = $this->imagelist[$img]['h']; + $channels = $this->imagelist[$img]['c']; + } else { + $tmp = getimagesize($img); + $imageWidth = $tmp[0]; + $imageHeight = $tmp[1]; + + if (isset($tmp['channels'])) { + $channels = $tmp['channels']; + } else { + $channels = 3; + } + + $data = file_get_contents($img); + } + + if ($w <= 0 && $h <= 0) { + $w = $imageWidth; + } + + if ($w == 0) { + $w = $h / $imageHeight * $imageWidth; + } + + if ($h == 0) { + $h = $w * $imageHeight / $imageWidth; + } + + $this->addJpegImage_common($data, $img, $imageWidth, $imageHeight, $x, $y, $w, $h, $channels); + } + + /** + * common code used by the two JPEG adding functions + * @param $data + * @param $imgname + * @param $imageWidth + * @param $imageHeight + * @param $x + * @param $y + * @param int $w + * @param int $h + * @param int $channels + */ + private function addJpegImage_common( + &$data, + $imgname, + $imageWidth, + $imageHeight, + $x, + $y, + $w = 0, + $h = 0, + $channels = 3 + ) { + if ($this->image_iscached($imgname)) { + $label = $this->imagelist[$imgname]['label']; + //debugpng + //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']'; + + } else { + if ($data == null) { + $this->addMessage('addJpegImage_common error - (' . $imgname . ') data not present!'); + + return; + } + + // note that this function is not to be called externally + // it is just the common code between the GD and the file options + $this->numImages++; + $im = $this->numImages; + $label = "I$im"; + $this->numObj++; + + $this->o_image( + $this->numObj, + 'new', + [ + 'label' => $label, + 'data' => &$data, + 'iw' => $imageWidth, + 'ih' => $imageHeight, + 'channels' => $channels + ] + ); + + $this->imagelist[$imgname] = [ + 'label' => $label, + 'w' => $imageWidth, + 'h' => $imageHeight, + 'c' => $channels + ]; + } + + $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ ", $w, $h, $x, $y, $label)); + } + + /** + * specify where the document should open when it first starts + * + * @param $style + * @param int $a + * @param int $b + * @param int $c + */ + function openHere($style, $a = 0, $b = 0, $c = 0) + { + // this function will open the document at a specified page, in a specified style + // the values for style, and the required parameters are: + // 'XYZ' left, top, zoom + // 'Fit' + // 'FitH' top + // 'FitV' left + // 'FitR' left,bottom,right + // 'FitB' + // 'FitBH' top + // 'FitBV' left + $this->numObj++; + $this->o_destination( + $this->numObj, + 'new', + ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c] + ); + $id = $this->catalogId; + $this->o_catalog($id, 'openHere', $this->numObj); + } + + /** + * Add JavaScript code to the PDF document + * + * @param string $code + */ + function addJavascript($code) + { + $this->javascript .= $code; + } + + /** + * create a labelled destination within the document + * + * @param $label + * @param $style + * @param int $a + * @param int $b + * @param int $c + */ + function addDestination($label, $style, $a = 0, $b = 0, $c = 0) + { + // associates the given label with the destination, it is done this way so that a destination can be specified after + // it has been linked to + // styles are the same as the 'openHere' function + $this->numObj++; + $this->o_destination( + $this->numObj, + 'new', + ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c] + ); + $id = $this->numObj; + + // store the label->idf relationship, note that this means that labels can be used only once + $this->destinations["$label"] = $id; + } + + /** + * define font families, this is used to initialize the font families for the default fonts + * and for the user to add new ones for their fonts. The default bahavious can be overridden should + * that be desired. + * + * @param $family + * @param string $options + */ + function setFontFamily($family, $options = '') + { + if (!is_array($options)) { + if ($family === 'init') { + // set the known family groups + // these font families will be used to enable bold and italic markers to be included + // within text streams. html forms will be used... + $this->fontFamilies['Helvetica.afm'] = + [ + 'b' => 'Helvetica-Bold.afm', + 'i' => 'Helvetica-Oblique.afm', + 'bi' => 'Helvetica-BoldOblique.afm', + 'ib' => 'Helvetica-BoldOblique.afm' + ]; + + $this->fontFamilies['Courier.afm'] = + [ + 'b' => 'Courier-Bold.afm', + 'i' => 'Courier-Oblique.afm', + 'bi' => 'Courier-BoldOblique.afm', + 'ib' => 'Courier-BoldOblique.afm' + ]; + + $this->fontFamilies['Times-Roman.afm'] = + [ + 'b' => 'Times-Bold.afm', + 'i' => 'Times-Italic.afm', + 'bi' => 'Times-BoldItalic.afm', + 'ib' => 'Times-BoldItalic.afm' + ]; + } + } else { + + // the user is trying to set a font family + // note that this can also be used to set the base ones to something else + if (mb_strlen($family)) { + $this->fontFamilies[$family] = $options; + } + } + } + + /** + * used to add messages for use in debugging + * + * @param $message + */ + function addMessage($message) + { + $this->messages .= $message . "\n"; + } + + /** + * a few functions which should allow the document to be treated transactionally. + * + * @param $action + */ + function transaction($action) + { + switch ($action) { + case 'start': + // store all the data away into the checkpoint variable + $data = get_object_vars($this); + $this->checkpoint = $data; + unset($data); + break; + + case 'commit': + if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])) { + $tmp = $this->checkpoint['checkpoint']; + $this->checkpoint = $tmp; + unset($tmp); + } else { + $this->checkpoint = ''; + } + break; + + case 'rewind': + // do not destroy the current checkpoint, but move us back to the state then, so that we can try again + if (is_array($this->checkpoint)) { + // can only abort if were inside a checkpoint + $tmp = $this->checkpoint; + + foreach ($tmp as $k => $v) { + if ($k !== 'checkpoint') { + $this->$k = $v; + } + } + unset($tmp); + } + break; + + case 'abort': + if (is_array($this->checkpoint)) { + // can only abort if were inside a checkpoint + $tmp = $this->checkpoint; + foreach ($tmp as $k => $v) { + $this->$k = $v; + } + unset($tmp); + } + break; + } + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php b/vendor/dompdf/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php new file mode 100644 index 0000000..7169e0e --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php @@ -0,0 +1,494 @@ +getDimensions(); + $w = $dimensions["width"]; + $h = $dimensions["height"]; + + if (!$canvas) { + $canvas = new \Svg\Surface\CPdf(array(0, 0, $w, $h)); + $refl = new \ReflectionClass($canvas); + $canvas->fontcache = realpath(dirname($refl->getFileName()) . "/../../fonts/")."/"; + } + + // Flip PDF coordinate system so that the origin is in + // the top left rather than the bottom left + $canvas->transform(array( + 1, 0, + 0, -1, + 0, $h + )); + + $this->width = $w; + $this->height = $h; + + $this->canvas = $canvas; + } + + function out() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + return $this->canvas->output(); + } + + public function save() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->save(); + } + + public function restore() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->restore(); + } + + public function scale($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->transform($x, 0, 0, $y, 0, 0); + } + + public function rotate($angle) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $a = deg2rad($angle); + $cos_a = cos($a); + $sin_a = sin($a); + + $this->transform( + $cos_a, $sin_a, + -$sin_a, $cos_a, + 0, 0 + ); + } + + public function translate($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->transform( + 1, 0, + 0, 1, + $x, $y + ); + } + + public function transform($a, $b, $c, $d, $e, $f) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->canvas->transform(array($a, $b, $c, $d, $e, $f)); + } + + public function beginPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + // TODO: Implement beginPath() method. + } + + public function closePath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->closePath(); + } + + public function fillStroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->fillStroke($close); + } + + public function clip() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->clip(); + } + + public function fillText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->addText($x, $y, $this->style->fontSize, $text); + } + + public function strokeText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->addText($x, $y, $this->style->fontSize, $text); + } + + public function drawImage($image, $sx, $sy, $sw = null, $sh = null, $dx = null, $dy = null, $dw = null, $dh = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + if (strpos($image, "data:") === 0) { + $parts = explode(',', $image, 2); + + $data = $parts[1]; + $base64 = false; + + $token = strtok($parts[0], ';'); + while ($token !== false) { + if ($token == 'base64') { + $base64 = true; + } + + $token = strtok(';'); + } + + if ($base64) { + $data = base64_decode($data); + } + } + else { + $data = file_get_contents($image); + } + + $image = tempnam(sys_get_temp_dir(), "svg"); + file_put_contents($image, $data); + + $img = $this->image($image, $sx, $sy, $sw, $sh, "normal"); + + + unlink($image); + } + + public static function getimagesize($filename) + { + static $cache = array(); + + if (isset($cache[$filename])) { + return $cache[$filename]; + } + + list($width, $height, $type) = getimagesize($filename); + + if ($width == null || $height == null) { + $data = file_get_contents($filename, null, null, 0, 26); + + if (substr($data, 0, 2) === "BM") { + $meta = unpack('vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight', $data); + $width = (int)$meta['width']; + $height = (int)$meta['height']; + $type = IMAGETYPE_BMP; + } + } + + return $cache[$filename] = array($width, $height, $type); + } + + function image($img, $x, $y, $w, $h, $resolution = "normal") + { + list($width, $height, $type) = $this->getimagesize($img); + + switch ($type) { + case IMAGETYPE_JPEG: + $this->canvas->addJpegFromFile($img, $x, $y - $h, $w, $h); + break; + + case IMAGETYPE_GIF: + case IMAGETYPE_BMP: + // @todo use cache for BMP and GIF + $img = $this->_convert_gif_bmp_to_png($img, $type); + + case IMAGETYPE_PNG: + $this->canvas->addPngFromFile($img, $x, $y - $h, $w, $h); + break; + + default: + } + } + + public function lineTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->lineTo($x, $y); + } + + public function moveTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->moveTo($x, $y); + } + + public function quadraticCurveTo($cpx, $cpy, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + // FIXME not accurate + $this->canvas->quadTo($cpx, $cpy, $x, $y); + } + + public function bezierCurveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->curveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y); + } + + public function arcTo($x1, $y1, $x2, $y2, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + } + + public function arc($x, $y, $radius, $startAngle, $endAngle, $anticlockwise = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radius, $radius, 0, 8, $startAngle, $endAngle, false, false, false, true); + } + + public function circle($x, $y, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radius, $radius, 0, 8, 0, 360, true, false, false, false); + } + + public function ellipse($x, $y, $radiusX, $radiusY, $rotation, $startAngle, $endAngle, $anticlockwise) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radiusX, $radiusY, 0, 8, 0, 360, false, false, false, false); + } + + public function fillRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->fill(); + } + + public function rect($x, $y, $w, $h, $rx = 0, $ry = 0) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $canvas = $this->canvas; + + if ($rx <= 0.000001/* && $ry <= 0.000001*/) { + $canvas->rect($x, $y, $w, $h); + + return; + } + + $rx = min($rx, $w / 2); + $rx = min($rx, $h / 2); + + /* Define a path for a rectangle with corners rounded by a given radius. + * Start from the lower left corner and proceed counterclockwise. + */ + $this->moveTo($x + $rx, $y); + + /* Start of the arc segment in the lower right corner */ + $this->lineTo($x + $w - $rx, $y); + + /* Arc segment in the lower right corner */ + $this->arc($x + $w - $rx, $y + $rx, $rx, 270, 360); + + /* Start of the arc segment in the upper right corner */ + $this->lineTo($x + $w, $y + $h - $rx ); + + /* Arc segment in the upper right corner */ + $this->arc($x + $w - $rx, $y + $h - $rx, $rx, 0, 90); + + /* Start of the arc segment in the upper left corner */ + $this->lineTo($x + $rx, $y + $h); + + /* Arc segment in the upper left corner */ + $this->arc($x + $rx, $y + $h - $rx, $rx, 90, 180); + + /* Start of the arc segment in the lower left corner */ + $this->lineTo($x , $y + $rx); + + /* Arc segment in the lower left corner */ + $this->arc($x + $rx, $y + $rx, $rx, 180, 270); + } + + public function fill() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->fill(); + } + + public function strokeRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->stroke(); + } + + public function stroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->stroke($close); + } + + public function endPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->endPath(); + } + + public function measureText($text) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $style = $this->getStyle(); + $this->setFont($style->fontFamily, $style->fontStyle, $style->fontWeight); + + return $this->canvas->getTextWidth($this->getStyle()->fontSize, $text); + } + + public function getStyle() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + return $this->style; + } + + public function setStyle(Style $style) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->style = $style; + $canvas = $this->canvas; + + if (is_array($style->stroke) && $stroke = $style->stroke) { + $canvas->setStrokeColor(array((float)$stroke[0]/255, (float)$stroke[1]/255, (float)$stroke[2]/255), true); + } + + if (is_array($style->fill) && $fill = $style->fill) { + $canvas->setColor(array((float)$fill[0]/255, (float)$fill[1]/255, (float)$fill[2]/255), true); + } + + if ($fillRule = strtolower($style->fillRule)) { + $canvas->setFillRule($fillRule); + } + + $opacity = $style->opacity; + if ($opacity !== null && $opacity < 1.0) { + $canvas->setLineTransparency("Normal", $opacity); + $canvas->currentLineTransparency = null; + + $canvas->setFillTransparency("Normal", $opacity); + $canvas->currentFillTransparency = null; + } + else { + $fillOpacity = $style->fillOpacity; + if ($fillOpacity !== null && $fillOpacity < 1.0) { + $canvas->setFillTransparency("Normal", $fillOpacity); + $canvas->currentFillTransparency = null; + } + + $strokeOpacity = $style->strokeOpacity; + if ($strokeOpacity !== null && $strokeOpacity < 1.0) { + $canvas->setLineTransparency("Normal", $strokeOpacity); + $canvas->currentLineTransparency = null; + } + } + + $dashArray = null; + if ($style->strokeDasharray) { + $dashArray = preg_split('/\s*,\s*/', $style->strokeDasharray); + } + + + $phase=0; + if ($style->strokeDashoffset) { + $phase = $style->strokeDashoffset; + } + + + $canvas->setLineStyle( + $style->strokeWidth, + $style->strokeLinecap, + $style->strokeLinejoin, + $dashArray, + $phase + ); + + $this->setFont($style->fontFamily, $style->fontStyle, $style->fontWeight); + } + + public function setFont($family, $style, $weight) + { + $map = [ + "serif" => "times", + "sans-serif" => "helvetica", + "fantasy" => "symbol", + "cursive" => "times", + "monospace" => "courier" + ]; + + $styleMap = [ + "courier" => [ + "" => "Courier", + "b" => "Courier-Bold", + "i" => "Courier-Oblique", + "bi" => "Courier-BoldOblique", + ], + "helvetica" => [ + "" => "Helvetica", + "b" => "Helvetica-Bold", + "i" => "Helvetica-Oblique", + "bi" => "Helvetica-BoldOblique", + ], + "symbol" => [ + "" => "Symbol" + ], + "times" => [ + "" => "Times-Roman", + "b" => "Times-Bold", + "i" => "Times-Italic", + "bi" => "Times-BoldItalic", + ], + ]; + + $family_lc = strtolower($family); + if (isset($map[$family_lc])) { + $family = $map[$family_lc]; + } + + if (isset($styleMap[$family])) { + $key = ""; + + $weight = strtolower($weight); + if ($weight === "bold" || $weight === "bolder" || (is_numeric($weight) && $weight >= 600)) { + $key .= "b"; + } + + $style = strtolower($style); + if ($style === "italic" || $style === "oblique") { + $key .= "i"; + } + + if (isset($styleMap[$family][$key])) { + $family = $styleMap[$family][$key]; + } + } + + $this->canvas->selectFont("$family.afm"); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Surface/SurfaceInterface.php b/vendor/dompdf/php-svg-lib/src/Svg/Surface/SurfaceInterface.php new file mode 100644 index 0000000..ed2807e --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Surface/SurfaceInterface.php @@ -0,0 +1,89 @@ +getDimensions(); + $w = $dimensions["width"]; + $h = $dimensions["height"]; + + if (!$canvas) { + $canvas = new \PDFlib(); + + /* all strings are expected as utf8 */ + $canvas->set_option("stringformat=utf8"); + $canvas->set_option("errorpolicy=return"); + + /* open new PDF file; insert a file name to create the PDF on disk */ + if ($canvas->begin_document("", "") == 0) { + die("Error: " . $canvas->get_errmsg()); + } + $canvas->set_info("Creator", "PDFlib starter sample"); + $canvas->set_info("Title", "starter_graphics"); + + $canvas->begin_page_ext($w, $h, ""); + } + + // Flip PDF coordinate system so that the origin is in + // the top left rather than the bottom left + $canvas->setmatrix( + 1, 0, + 0, -1, + 0, $h + ); + + $this->width = $w; + $this->height = $h; + + $this->canvas = $canvas; + } + + function out() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->canvas->end_page_ext(""); + $this->canvas->end_document(""); + + return $this->canvas->get_buffer(); + } + + public function save() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->save(); + } + + public function restore() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->restore(); + } + + public function scale($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->scale($x, $y); + } + + public function rotate($angle) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->rotate($angle); + } + + public function translate($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->translate($x, $y); + } + + public function transform($a, $b, $c, $d, $e, $f) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->concat($a, $b, $c, $d, $e, $f); + } + + public function beginPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + // TODO: Implement beginPath() method. + } + + public function closePath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->closepath(); + } + + public function fillStroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + if ($close) { + $this->canvas->closepath_fill_stroke(); + } else { + $this->canvas->fill_stroke(); + } + } + + public function clip() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->clip(); + } + + public function fillText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->set_text_pos($x, $y); + $this->canvas->show($text); + } + + public function strokeText($text, $x, $y, $maxWidth = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + // TODO: Implement drawImage() method. + } + + public function drawImage($image, $sx, $sy, $sw = null, $sh = null, $dx = null, $dy = null, $dw = null, $dh = null) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + if (strpos($image, "data:") === 0) { + $data = substr($image, strpos($image, ";") + 1); + if (strpos($data, "base64") === 0) { + $data = base64_decode(substr($data, 7)); + } + } + else { + $data = file_get_contents($image); + } + + $image = tempnam(sys_get_temp_dir(), "svg"); + file_put_contents($image, $data); + + $img = $this->canvas->load_image("auto", $image, ""); + + $sy = $sy - $sh; + $this->canvas->fit_image($img, $sx, $sy, 'boxsize={' . "$sw $sh" . '} fitmethod=entire'); + + unlink($image); + } + + public function lineTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->lineto($x, $y); + } + + public function moveTo($x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->moveto($x, $y); + } + + public function quadraticCurveTo($cpx, $cpy, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + // FIXME not accurate + $this->canvas->curveTo($cpx, $cpy, $cpx, $cpy, $x, $y); + } + + public function bezierCurveTo($cp1x, $cp1y, $cp2x, $cp2y, $x, $y) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->curveto($cp1x, $cp1y, $cp2x, $cp2y, $x, $y); + } + + public function arcTo($x1, $y1, $x2, $y2, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + } + + public function arc($x, $y, $radius, $startAngle, $endAngle, $anticlockwise = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->arc($x, $y, $radius, $startAngle, $endAngle); + } + + public function circle($x, $y, $radius) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->circle($x, $y, $radius); + } + + public function ellipse($x, $y, $radiusX, $radiusY, $rotation, $startAngle, $endAngle, $anticlockwise) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->ellipse($x, $y, $radiusX, $radiusY); + } + + public function fillRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->fill(); + } + + public function rect($x, $y, $w, $h, $rx = 0, $ry = 0) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $canvas = $this->canvas; + + if ($rx <= 0.000001/* && $ry <= 0.000001*/) { + $canvas->rect($x, $y, $w, $h); + + return; + } + + /* Define a path for a rectangle with corners rounded by a given radius. + * Start from the lower left corner and proceed counterclockwise. + */ + $canvas->moveto($x + $rx, $y); + + /* Start of the arc segment in the lower right corner */ + $canvas->lineto($x + $w - $rx, $y); + + /* Arc segment in the lower right corner */ + $canvas->arc($x + $w - $rx, $y + $rx, $rx, 270, 360); + + /* Start of the arc segment in the upper right corner */ + $canvas->lineto($x + $w, $y + $h - $rx ); + + /* Arc segment in the upper right corner */ + $canvas->arc($x + $w - $rx, $y + $h - $rx, $rx, 0, 90); + + /* Start of the arc segment in the upper left corner */ + $canvas->lineto($x + $rx, $y + $h); + + /* Arc segment in the upper left corner */ + $canvas->arc($x + $rx, $y + $h - $rx, $rx, 90, 180); + + /* Start of the arc segment in the lower left corner */ + $canvas->lineto($x , $y + $rx); + + /* Arc segment in the lower left corner */ + $canvas->arc($x + $rx, $y + $rx, $rx, 180, 270); + } + + public function fill() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->fill(); + } + + public function strokeRect($x, $y, $w, $h) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->rect($x, $y, $w, $h); + $this->stroke(); + } + + public function stroke(bool $close = false) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + if ($close) { + $this->canvas->closepath_stroke(); + } else { + $this->canvas->stroke(); + } + } + + public function endPath() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $this->canvas->endPath(); + } + + public function measureText($text) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + $style = $this->getStyle(); + $font = $this->getFont($style->fontFamily, $style->fontStyle); + + return $this->canvas->stringwidth($text, $font, $this->getStyle()->fontSize); + } + + public function getStyle() + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + return $this->style; + } + + public function setStyle(Style $style) + { + if (self::DEBUG) echo __FUNCTION__ . "\n"; + + $this->style = $style; + $canvas = $this->canvas; + + if (is_array($style->stroke) && $stroke = $style->stroke) { + $canvas->setcolor( + "stroke", + "rgb", + $stroke[0] / 255, + $stroke[1] / 255, + $stroke[2] / 255, + null + ); + } + + if (is_array($style->fill) && $fill = $style->fill) { + $canvas->setcolor( + "fill", + "rgb", + $fill[0] / 255, + $fill[1] / 255, + $fill[2] / 255, + null + ); + } + + if ($fillRule = strtolower($style->fillRule)) { + $map = array( + "nonzero" => "winding", + "evenodd" => "evenodd", + ); + + if (isset($map[$fillRule])) { + $fillRule = $map[$fillRule]; + + $canvas->set_parameter("fillrule", $fillRule); + } + } + + $opts = array(); + if ($style->strokeWidth > 0.000001) { + $opts[] = "linewidth=$style->strokeWidth"; + } + + if (in_array($style->strokeLinecap, array("butt", "round", "projecting"))) { + $opts[] = "linecap=$style->strokeLinecap"; + } + + if (in_array($style->strokeLinejoin, array("miter", "round", "bevel"))) { + $opts[] = "linejoin=$style->strokeLinejoin"; + } + + $canvas->set_graphics_option(implode(" ", $opts)); + + $opts = array(); + $opacity = $style->opacity; + if ($opacity !== null && $opacity < 1.0) { + $opts[] = "opacityfill=$opacity"; + $opts[] = "opacitystroke=$opacity"; + } + else { + $fillOpacity = $style->fillOpacity; + if ($fillOpacity !== null && $fillOpacity < 1.0) { + $opts[] = "opacityfill=$fillOpacity"; + } + + $strokeOpacity = $style->strokeOpacity; + if ($strokeOpacity !== null && $strokeOpacity < 1.0) { + $opts[] = "opacitystroke=$strokeOpacity"; + } + } + + if (count($opts)) { + $gs = $canvas->create_gstate(implode(" ", $opts)); + $canvas->set_gstate($gs); + } + + $font = $this->getFont($style->fontFamily, $style->fontStyle); + if ($font) { + $canvas->setfont($font, $style->fontSize); + } + } + + private function getFont($family, $style) + { + $map = array( + "serif" => "Times", + "sans-serif" => "Helvetica", + "fantasy" => "Symbol", + "cursive" => "Times", + "monospace" => "Courier", + + "arial" => "Helvetica", + "verdana" => "Helvetica", + ); + + $family = strtolower($family); + if (isset($map[$family])) { + $family = $map[$family]; + } + + return $this->canvas->load_font($family, "unicode", "fontstyle=$style"); + } + + public function setFont($family, $style, $weight) + { + // TODO: Implement setFont() method. + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/AbstractTag.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/AbstractTag.php new file mode 100644 index 0000000..872d971 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/AbstractTag.php @@ -0,0 +1,326 @@ +document = $document; + $this->tagName = $tagName; + } + + public function getDocument(){ + return $this->document; + } + + /** + * @return Group|null + */ + public function getParentGroup() { + $stack = $this->getDocument()->getStack(); + for ($i = count($stack)-2; $i >= 0; $i--) { + $tag = $stack[$i]; + + if ($tag instanceof Group || $tag instanceof Document) { + return $tag; + } + } + + return null; + } + + public function handle($attributes) + { + $this->attributes = $attributes; + + if (!$this->getDocument()->inDefs || $this instanceof StyleTag) { + $this->before($attributes); + $this->start($attributes); + } + } + + public function handleEnd() + { + if (!$this->getDocument()->inDefs || $this instanceof StyleTag) { + $this->end(); + $this->after(); + } + } + + protected function before($attributes) + { + } + + protected function start($attributes) + { + } + + protected function end() + { + } + + protected function after() + { + } + + public function getAttributes() + { + return $this->attributes; + } + + protected function setStyle(Style $style) + { + $this->style = $style; + + if ($style->display === "none") { + $this->hasShape = false; + } + } + + /** + * @return Style + */ + public function getStyle() + { + return $this->style; + } + + /** + * Make a style object from the tag and its attributes + * + * @param array $attributes + * + * @return Style + */ + protected function makeStyle($attributes) { + $style = new Style($this->document); + $style->inherit($this); + $style->fromStyleSheets($this, $attributes); + $style->fromAttributes($attributes); + + return $style; + } + + protected function applyTransform($attributes) + { + + if (isset($attributes["transform"])) { + $surface = $this->document->getSurface(); + + $transform = $attributes["transform"]; + + $matches = array(); + preg_match_all( + '/(matrix|translate|scale|rotate|skew|skewX|skewY)\((.*?)\)/is', + $transform, + $matches, + PREG_SET_ORDER + ); + + $transformations = array(); + foreach ($matches as $match) { + $arguments = preg_split('/[ ,]+/', $match[2]); + array_unshift($arguments, $match[1]); + $transformations[] = $arguments; + } + + foreach ($transformations as $t) { + switch ($t[0]) { + case "matrix": + $surface->transform($t[1], $t[2], $t[3], $t[4], $t[5], $t[6]); + break; + + case "translate": + $surface->translate($t[1], isset($t[2]) ? $t[2] : 0); + break; + + case "scale": + $surface->scale($t[1], isset($t[2]) ? $t[2] : $t[1]); + break; + + case "rotate": + if (isset($t[2])) { + $t[3] = isset($t[3]) ? $t[3] : 0; + $surface->translate($t[2], $t[3]); + $surface->rotate($t[1]); + $surface->translate(-$t[2], -$t[3]); + } else { + $surface->rotate($t[1]); + } + break; + + case "skewX": + $tan_x = tan(deg2rad($t[1])); + $surface->transform(1, 0, $tan_x, 1, 0, 0); + break; + + case "skewY": + $tan_y = tan(deg2rad($t[1])); + $surface->transform(1, $tan_y, 0, 1, 0, 0); + break; + } + } + } + } + + /** + * Apply a viewBox transform to the element + * + * @param array $attributes + */ + protected function applyViewbox($attributes) { + if (!isset($attributes["viewbox"])) { + return; + } + + $surface = $this->document->getSurface(); + $viewBox = preg_split('/[\s,]+/is', trim($attributes['viewbox'])); + if (count($viewBox) != 4) { + return; + } + + // Computing the equivalent transform of an SVG viewport + // https://svgwg.org/svg2-draft/coords.html#ComputingAViewportsTransform + + // 1. Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute respectively. + [$vbX, $vbY, $vbWidth, $vbHeight] = $viewBox; + + if ($vbWidth < 0 || $vbHeight < 0) { + return; + } + + // correct solution is to not render, for now scaling to 0 below + //if ($vbWidth == 0 || $vbHeight == 0) { + //} + + // 2. Let e-x, e-y, e-width, e-height be the position and size of the element respectively. + $eX = $attributes["x"] ?? 0; + $eY = $attributes["y"] ?? 0; + $eWidth = $attributes["width"] ?? $this->document->getWidth(); + $eHeight = $attributes["height"] ?? $this->document->getHeight(); + + // 3. Let align be the align value of preserveAspectRatio, or 'xMidYMid' if preserveAspectRatio is not defined. + $preserveAspectRatio = explode(" ", $attributes["preserveAspectRatio"] ?? "xMidYMid meet"); + $align = $preserveAspectRatio[0]; + + // 4. Let meetOrSlice be the meetOrSlice value of preserveAspectRatio, or 'meet' if preserveAspectRatio is not defined or if meetOrSlice is missing from this value. + $meetOrSlice = $meetOrSlice ?? "meet"; + + // 5. Initialize scale-x to e-width/vb-width. + $scaleX = $vbWidth == 0 ? 0 : ($eWidth / $vbWidth); + + // 6. Initialize scale-y to e-height/vb-height. + $scaleY = $vbHeight == 0 ? 0 : ($eHeight / $vbHeight); + + // 7. If align is not 'none' and meetOrSlice is 'meet', set the larger of scale-x and scale-y to the smaller. + if ($align !== "none" && $meetOrSlice === "meet") { + $scaleX = min($scaleX, $scaleY); + $scaleY = min($scaleX, $scaleY); + } + + // 8. Otherwise, if align is not 'none' and meetOrSlice is 'slice', set the smaller of scale-x and scale-y to the larger. + elseif ($align !== "none" && $meetOrSlice === "slice") { + $scaleX = max($scaleX, $scaleY); + $scaleY = max($scaleX, $scaleY); + } + + // 9. Initialize translate-x to e-x - (vb-x * scale-x). + $translateX = $eX - ($vbX * $scaleX); + + // 10. Initialize translate-y to e-y - (vb-y * scale-y) + $translateY = $eY - ($vbY * $scaleY); + + // 11. If align contains 'xMid', add (e-width - vb-width * scale-x) / 2 to translate-x. + if (strpos($align, "xMid") !== false) { + $translateX += ($eWidth - $vbWidth * $scaleX) / 2; + } + + // 12. If align contains 'xMax', add (e-width - vb-width * scale-x) to translate-x. + if (strpos($align, "xMax") !== false) { + $translateX += ($eWidth - $vbWidth * $scaleX); + } + + // 13. If align contains 'yMid', add (e-height - vb-height * scale-y) / 2 to translate-y. + if (strpos($align, "yMid") !== false) { + $translateX += ($eHeight - $vbHeight * $scaleY) / 2; + } + + // 14. If align contains 'yMax', add (e-height - vb-height * scale-y) to translate-y. + if (strpos($align, "yMid") !== false) { + $translateX += ($eHeight - $vbHeight * $scaleY); + } + + $surface->translate($translateX, $translateY); + $surface->scale($scaleX, $scaleY); + } + + /** + * Convert the given size for the context of this current tag. + * Takes a pixel-based reference, which is usually specific to the context of the size, + * but the actual reference size will be decided based upon the unit used. + * + * @param string $size + * @param float $pxReference + * + * @return float + */ + protected function convertSize(string $size, float $pxReference): float + { + $length = new CssLength($size); + $reference = $pxReference; + $defaultFontSize = 12; + + switch ($length->getUnit()) { + case "em": + $reference = $this->style->fontSize ?? $defaultFontSize; + break; + case "rem": + $reference = $this->document->style->fontSize ?? $defaultFontSize; + break; + case "ex": + case "ch": + $emRef = $this->style->fontSize ?? $defaultFontSize; + $reference = $emRef * 0.5; + break; + case "vw": + $reference = $this->getDocument()->getWidth(); + break; + case "vh": + $reference = $this->getDocument()->getHeight(); + break; + case "vmin": + $reference = min($this->getDocument()->getHeight(), $this->getDocument()->getWidth()); + break; + case "vmax": + $reference = max($this->getDocument()->getHeight(), $this->getDocument()->getWidth()); + break; + } + + return (new CssLength($size))->toPixels($reference); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Anchor.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Anchor.php new file mode 100644 index 0000000..349bfa8 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Anchor.php @@ -0,0 +1,13 @@ +document->getWidth(); + $this->cx = $this->convertSize($attributes['cx'], $width); + } + if (isset($attributes['cy'])) { + $height = $this->document->getHeight(); + $this->cy = $this->convertSize($attributes['cy'], $height); + } + if (isset($attributes['r'])) { + $diagonal = $this->document->getDiagonal(); + $this->r = $this->convertSize($attributes['r'], $diagonal); + } + + $this->document->getSurface()->circle($this->cx, $this->cy, $this->r); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/ClipPath.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/ClipPath.php new file mode 100644 index 0000000..991a4db --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/ClipPath.php @@ -0,0 +1,32 @@ +document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyTransform($attributes); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Ellipse.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Ellipse.php new file mode 100644 index 0000000..f61dfd2 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Ellipse.php @@ -0,0 +1,41 @@ +document->getWidth(); + $height = $this->document->getHeight(); + + if (isset($attributes['cx'])) { + $this->cx = $this->convertSize($attributes['cx'], $width); + } + if (isset($attributes['cy'])) { + $this->cy = $this->convertSize($attributes['cy'], $height); + } + if (isset($attributes['rx'])) { + $this->rx = $this->convertSize($attributes['rx'], $width); + } + if (isset($attributes['ry'])) { + $this->ry = $this->convertSize($attributes['ry'], $height); + } + + $this->document->getSurface()->ellipse($this->cx, $this->cy, $this->rx, $this->ry, 0, 0, 360, false); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Group.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Group.php new file mode 100644 index 0000000..73d7a45 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Group.php @@ -0,0 +1,32 @@ +document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyTransform($attributes); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Image.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Image.php new file mode 100644 index 0000000..45d51e5 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Image.php @@ -0,0 +1,75 @@ +document->getSurface(); + $surface->save(); + + $this->applyTransform($attributes); + } + + public function start($attributes) + { + $height = $this->document->getHeight(); + $width = $this->document->getWidth(); + $this->y = $height; + + if (isset($attributes['x'])) { + $this->x = $this->convertSize($attributes['x'], $width); + } + if (isset($attributes['y'])) { + $this->y = $height - $this->convertSize($attributes['y'], $height); + } + + if (isset($attributes['width'])) { + $this->width = $this->convertSize($attributes['width'], $width); + } + if (isset($attributes['height'])) { + $this->height = $this->convertSize($attributes['height'], $height); + } + + if (isset($attributes['xlink:href'])) { + $this->href = $attributes['xlink:href']; + } + + if (isset($attributes['href'])) { + $this->href = $attributes['href']; + } + + $this->document->getSurface()->transform(1, 0, 0, -1, 0, $height); + + $scheme = \strtolower(parse_url($this->href, PHP_URL_SCHEME) ?: ""); + if ( + $scheme === "phar" || \strtolower(\substr($this->href, 0, 7)) === "phar://" + || ($this->document->allowExternalReferences === false && $scheme !== "data") + ) { + return; + } + + $this->document->getSurface()->drawImage($this->href, $this->x, $this->y, $this->width, $this->height); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Line.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Line.php new file mode 100644 index 0000000..4f6a276 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Line.php @@ -0,0 +1,42 @@ +document->getHeight(); + $width = $this->document->getWidth(); + + if (isset($attributes['x1'])) { + $this->x1 = $this->convertSize($attributes['x1'], $width); + } + if (isset($attributes['y1'])) { + $this->y1 = $this->convertSize($attributes['y1'], $height); + } + if (isset($attributes['x2'])) { + $this->x2 = $this->convertSize($attributes['x2'], $width); + } + if (isset($attributes['y2'])) { + $this->y2 = $this->convertSize($attributes['y2'], $height); + } + + $surface = $this->document->getSurface(); + $surface->moveTo($this->x1, $this->y1); + $surface->lineTo($this->x2, $this->y2); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/LinearGradient.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/LinearGradient.php new file mode 100644 index 0000000..2a42686 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/LinearGradient.php @@ -0,0 +1,82 @@ +x1 = $attributes['x1']; + } + if (isset($attributes['y1'])) { + $this->y1 = $attributes['y1']; + } + if (isset($attributes['x2'])) { + $this->x2 = $attributes['x2']; + } + if (isset($attributes['y2'])) { + $this->y2 = $attributes['y2']; + } + } + + public function getStops() { + if (empty($this->stops)) { + foreach ($this->children as $_child) { + if ($_child->tagName != "stop") { + continue; + } + + $_stop = new Gradient\Stop(); + $_attributes = $_child->attributes; + + // Style + if (isset($_attributes["style"])) { + $_style = Style::parseCssStyle($_attributes["style"]); + + if (isset($_style["stop-color"])) { + $_stop->color = Style::parseColor($_style["stop-color"]); + } + + if (isset($_style["stop-opacity"])) { + $_stop->opacity = max(0, min(1.0, $_style["stop-opacity"])); + } + } + + // Attributes + if (isset($_attributes["offset"])) { + $_stop->offset = $_attributes["offset"]; + } + if (isset($_attributes["stop-color"])) { + $_stop->color = Style::parseColor($_attributes["stop-color"]); + } + if (isset($_attributes["stop-opacity"])) { + $_stop->opacity = max(0, min(1.0, $_attributes["stop-opacity"])); + } + + $this->stops[] = $_stop; + } + } + + return $this->stops; + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Path.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Path.php new file mode 100644 index 0000000..b792b3a --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Path.php @@ -0,0 +1,580 @@ + 2, + 'l' => 2, + 'h' => 1, + 'v' => 1, + 'c' => 6, + 's' => 4, + 'q' => 4, + 't' => 2, + 'a' => 7, + ); + + static $repeatedCommands = array( + 'm' => 'l', + 'M' => 'L', + ); + + public static function parse(string $commandSequence): array + { + $commands = array(); + preg_match_all('/([MZLHVCSQTAmzlhvcsqta])([eE ,\-.\d]+)*/', $commandSequence, $commands, PREG_SET_ORDER); + + $path = array(); + foreach ($commands as $c) { + if (count($c) == 3) { + $commandLower = strtolower($c[1]); + + // arcs have special flags that apparently don't require spaces. + if ($commandLower === 'a' && preg_match_all(static::ARC_REGEXP, $c[2], $matches, PREG_PATTERN_ORDER)) { + $numberOfMatches = count($matches[0]); + for ($k = 0; $k < $numberOfMatches; ++$k) { + $path[] = [ + $c[1], + $matches[1][$k], + $matches[2][$k], + $matches[3][$k], + $matches[4][$k], + $matches[5][$k], + $matches[6][$k], + $matches[7][$k], + ]; + } + continue; + } + + $arguments = array(); + preg_match_all('/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/i', $c[2], $arguments, PREG_PATTERN_ORDER); + $item = $arguments[0]; + + if ( + isset(self::$commandLengths[$commandLower]) && + ($commandLength = self::$commandLengths[$commandLower]) && + count($item) > $commandLength + ) { + $repeatedCommand = isset(self::$repeatedCommands[$c[1]]) ? self::$repeatedCommands[$c[1]] : $c[1]; + $command = $c[1]; + + for ($k = 0, $klen = count($item); $k < $klen; $k += $commandLength) { + $_item = array_slice($item, $k, $k + $commandLength); + array_unshift($_item, $command); + $path[] = $_item; + + $command = $repeatedCommand; + } + } else { + array_unshift($item, $c[1]); + $path[] = $item; + } + + } else { + $item = array($c[1]); + + $path[] = $item; + } + } + + return $path; + } + + public function start($attributes) + { + if (!isset($attributes['d'])) { + $this->hasShape = false; + + return; + } + + $path = static::parse($attributes['d']); + $surface = $this->document->getSurface(); + + // From https://github.com/kangax/fabric.js/blob/master/src/shapes/path.class.js + $current = null; // current instruction + $previous = null; + $subpathStartX = 0; + $subpathStartY = 0; + $x = 0; // current x + $y = 0; // current y + $controlX = 0; // current control point x + $controlY = 0; // current control point y + $tempX = null; + $tempY = null; + $tempControlX = null; + $tempControlY = null; + $l = 0; //-((this.width / 2) + $this.pathOffset.x), + $t = 0; //-((this.height / 2) + $this.pathOffset.y), + + foreach ($path as $current) { + switch ($current[0]) { // first letter + case 'l': // lineto, relative + $x += $current[1]; + $y += $current[2]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'L': // lineto, absolute + $x = $current[1]; + $y = $current[2]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'h': // horizontal lineto, relative + $x += $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'H': // horizontal lineto, absolute + $x = $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'v': // vertical lineto, relative + $y += $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'V': // verical lineto, absolute + $y = $current[1]; + $surface->lineTo($x + $l, $y + $t); + break; + + case 'm': // moveTo, relative + $x += $current[1]; + $y += $current[2]; + $subpathStartX = $x; + $subpathStartY = $y; + $surface->moveTo($x + $l, $y + $t); + break; + + case 'M': // moveTo, absolute + $x = $current[1]; + $y = $current[2]; + $subpathStartX = $x; + $subpathStartY = $y; + $surface->moveTo($x + $l, $y + $t); + break; + + case 'c': // bezierCurveTo, relative + $tempX = $x + $current[5]; + $tempY = $y + $current[6]; + $controlX = $x + $current[3]; + $controlY = $y + $current[4]; + $surface->bezierCurveTo( + $x + $current[1] + $l, // x1 + $y + $current[2] + $t, // y1 + $controlX + $l, // x2 + $controlY + $t, // y2 + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'C': // bezierCurveTo, absolute + $x = $current[5]; + $y = $current[6]; + $controlX = $current[3]; + $controlY = $current[4]; + $surface->bezierCurveTo( + $current[1] + $l, + $current[2] + $t, + $controlX + $l, + $controlY + $t, + $x + $l, + $y + $t + ); + break; + + case 's': // shorthand cubic bezierCurveTo, relative + + // transform to absolute x,y + $tempX = $x + $current[3]; + $tempY = $y + $current[4]; + + if (!preg_match('/[CcSs]/', $previous[0])) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + $controlX = $x; + $controlY = $y; + } else { + // calculate reflection of previous control points + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } + + $surface->bezierCurveTo( + $controlX + $l, + $controlY + $t, + $x + $current[1] + $l, + $y + $current[2] + $t, + $tempX + $l, + $tempY + $t + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + $controlX = $x + $current[1]; + $controlY = $y + $current[2]; + + $x = $tempX; + $y = $tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + $tempX = $current[3]; + $tempY = $current[4]; + + if (!preg_match('/[CcSs]/', $previous[0])) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + $controlX = $x; + $controlY = $y; + } else { + // calculate reflection of previous control points + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } + + $surface->bezierCurveTo( + $controlX + $l, + $controlY + $t, + $current[1] + $l, + $current[2] + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + $controlX = $current[1]; + $controlY = $current[2]; + + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + $tempX = $x + $current[3]; + $tempY = $y + $current[4]; + + $controlX = $x + $current[1]; + $controlY = $y + $current[2]; + + $surface->quadraticCurveTo( + $controlX + $l, + $controlY + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + $tempX = $current[3]; + $tempY = $current[4]; + + $surface->quadraticCurveTo( + $current[1] + $l, + $current[2] + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + $controlX = $current[1]; + $controlY = $current[2]; + break; + + case 't': // shorthand quadraticCurveTo, relative + + // transform to absolute x,y + $tempX = $x + $current[1]; + $tempY = $y + $current[2]; + + // calculate reflection of previous control points + if (preg_match('/[QqT]/', $previous[0])) { + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } elseif ($previous[0] === 't') { + $controlX = 2 * $x - $tempControlX; + $controlY = 2 * $y - $tempControlY; + } else { + $controlX = $x; + $controlY = $y; + } + + $tempControlX = $controlX; + $tempControlY = $controlY; + + $surface->quadraticCurveTo( + $controlX + $l, + $controlY + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'T': + $tempX = $current[1]; + $tempY = $current[2]; + + // calculate reflection of previous control points + if (preg_match('/[QqTt]/', $previous[0])) { + $controlX = 2 * $x - $controlX; + $controlY = 2 * $y - $controlY; + } else { + $controlX = $x; + $controlY = $y; + } + + $surface->quadraticCurveTo( + $controlX + $l, + $controlY + $t, + $tempX + $l, + $tempY + $t + ); + $x = $tempX; + $y = $tempY; + break; + + case 'a': + $this->drawArc( + $surface, + $x + $l, + $y + $t, + array( + $current[1], + $current[2], + $current[3], + $current[4], + $current[5], + $current[6] + $x + $l, + $current[7] + $y + $t + ) + ); + $x += $current[6]; + $y += $current[7]; + break; + + case 'A': + // TODO: optimize this + $this->drawArc( + $surface, + $x + $l, + $y + $t, + array( + $current[1], + $current[2], + $current[3], + $current[4], + $current[5], + $current[6] + $l, + $current[7] + $t + ) + ); + $x = $current[6]; + $y = $current[7]; + break; + + case 'z': + case 'Z': + $x = $subpathStartX; + $y = $subpathStartY; + $surface->closePath(); + break; + } + $previous = $current; + } + } + + function drawArc(SurfaceInterface $surface, $fx, $fy, $coords) + { + $rx = $coords[0]; + $ry = $coords[1]; + $rot = $coords[2]; + $large = $coords[3]; + $sweep = $coords[4]; + $tx = $coords[5]; + $ty = $coords[6]; + $segs = array( + array(), + array(), + array(), + array(), + ); + + $toX = $tx - $fx; + $toY = $ty - $fy; + + if ((float)($toX + $toY) === 0.0) { + return; + } + + if ((float)abs($rx) == 0.0 || (float)abs($ry) === 0.0) { + $surface->lineTo($tx, $ty); + return; + } + + $segsNorm = $this->arcToSegments($toX, $toY, $rx, $ry, $large, $sweep, $rot); + + for ($i = 0, $len = count($segsNorm); $i < $len; $i++) { + $segs[$i][0] = $segsNorm[$i][0] + $fx; + $segs[$i][1] = $segsNorm[$i][1] + $fy; + $segs[$i][2] = $segsNorm[$i][2] + $fx; + $segs[$i][3] = $segsNorm[$i][3] + $fy; + $segs[$i][4] = $segsNorm[$i][4] + $fx; + $segs[$i][5] = $segsNorm[$i][5] + $fy; + + call_user_func_array(array($surface, "bezierCurveTo"), $segs[$i]); + } + } + + function arcToSegments($toX, $toY, $rx, $ry, $large, $sweep, $rotateX) + { + $th = $rotateX * M_PI / 180; + $sinTh = sin($th); + $cosTh = cos($th); + $fromX = 0; + $fromY = 0; + + $rx = abs($rx); + $ry = abs($ry); + + $px = -$cosTh * $toX * 0.5 - $sinTh * $toY * 0.5; + $py = -$cosTh * $toY * 0.5 + $sinTh * $toX * 0.5; + $rx2 = $rx * $rx; + $ry2 = $ry * $ry; + $py2 = $py * $py; + $px2 = $px * $px; + $pl = $rx2 * $ry2 - $rx2 * $py2 - $ry2 * $px2; + $root = 0; + + if ($pl < 0) { + $s = sqrt(1 - $pl / ($rx2 * $ry2)); + $rx *= $s; + $ry *= $s; + } else { + $root = ($large == $sweep ? -1.0 : 1.0) * sqrt($pl / ($rx2 * $py2 + $ry2 * $px2)); + } + + $cx = $root * $rx * $py / $ry; + $cy = -$root * $ry * $px / $rx; + $cx1 = $cosTh * $cx - $sinTh * $cy + $toX * 0.5; + $cy1 = $sinTh * $cx + $cosTh * $cy + $toY * 0.5; + $mTheta = $this->calcVectorAngle(1, 0, ($px - $cx) / $rx, ($py - $cy) / $ry); + $dtheta = $this->calcVectorAngle(($px - $cx) / $rx, ($py - $cy) / $ry, (-$px - $cx) / $rx, (-$py - $cy) / $ry); + + if ($sweep == 0 && $dtheta > 0) { + $dtheta -= 2 * M_PI; + } else { + if ($sweep == 1 && $dtheta < 0) { + $dtheta += 2 * M_PI; + } + } + + // $Convert $into $cubic $bezier $segments <= 90deg + $segments = ceil(abs($dtheta / M_PI * 2)); + $result = array(); + $mDelta = $dtheta / $segments; + $mT = 8 / 3 * sin($mDelta / 4) * sin($mDelta / 4) / sin($mDelta / 2); + $th3 = $mTheta + $mDelta; + + for ($i = 0; $i < $segments; $i++) { + $result[$i] = $this->segmentToBezier( + $mTheta, + $th3, + $cosTh, + $sinTh, + $rx, + $ry, + $cx1, + $cy1, + $mT, + $fromX, + $fromY + ); + $fromX = $result[$i][4]; + $fromY = $result[$i][5]; + $mTheta = $th3; + $th3 += $mDelta; + } + + return $result; + } + + function segmentToBezier($th2, $th3, $cosTh, $sinTh, $rx, $ry, $cx1, $cy1, $mT, $fromX, $fromY) + { + $costh2 = cos($th2); + $sinth2 = sin($th2); + $costh3 = cos($th3); + $sinth3 = sin($th3); + $toX = $cosTh * $rx * $costh3 - $sinTh * $ry * $sinth3 + $cx1; + $toY = $sinTh * $rx * $costh3 + $cosTh * $ry * $sinth3 + $cy1; + $cp1X = $fromX + $mT * (-$cosTh * $rx * $sinth2 - $sinTh * $ry * $costh2); + $cp1Y = $fromY + $mT * (-$sinTh * $rx * $sinth2 + $cosTh * $ry * $costh2); + $cp2X = $toX + $mT * ($cosTh * $rx * $sinth3 + $sinTh * $ry * $costh3); + $cp2Y = $toY + $mT * ($sinTh * $rx * $sinth3 - $cosTh * $ry * $costh3); + + return array( + $cp1X, + $cp1Y, + $cp2X, + $cp2Y, + $toX, + $toY + ); + } + + function calcVectorAngle($ux, $uy, $vx, $vy) + { + $ta = atan2($uy, $ux); + $tb = atan2($vy, $vx); + if ($tb >= $ta) { + return $tb - $ta; + } else { + return 2 * M_PI - ($ta - $tb); + } + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Polygon.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Polygon.php new file mode 100644 index 0000000..72437af --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Polygon.php @@ -0,0 +1,41 @@ +document->getSurface(); + list($x, $y) = $points; + $surface->moveTo($x, $y); + + for ($i = 2; $i < $count; $i += 2) { + if ($i + 1 === $count) { + // invalid trailing point + continue; + } + $x = $points[$i]; + $y = $points[$i + 1]; + $surface->lineTo($x, $y); + } + + $surface->closePath(); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Polyline.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Polyline.php new file mode 100644 index 0000000..fce3458 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Polyline.php @@ -0,0 +1,39 @@ +document->getSurface(); + list($x, $y) = $points; + $surface->moveTo($x, $y); + + for ($i = 2; $i < $count; $i += 2) { + if ($i + 1 === $count) { + // invalid trailing point + continue; + } + $x = $points[$i]; + $y = $points[$i + 1]; + $surface->lineTo($x, $y); + } + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/RadialGradient.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/RadialGradient.php new file mode 100644 index 0000000..80c9879 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/RadialGradient.php @@ -0,0 +1,16 @@ +document->getWidth(); + $height = $this->document->getHeight(); + + if (isset($attributes['x'])) { + $this->x = $this->convertSize($attributes['x'], $width); + } + if (isset($attributes['y'])) { + $this->y = $this->convertSize($attributes['y'], $height); + } + + if (isset($attributes['width'])) { + $this->width = $this->convertSize($attributes['width'], $width); + } + if (isset($attributes['height'])) { + $this->height = $this->convertSize($attributes['height'], $height); + } + + if (isset($attributes['rx'])) { + $this->rx = $attributes['rx']; + } + if (isset($attributes['ry'])) { + $this->ry = $attributes['ry']; + } + + $this->document->getSurface()->rect($this->x, $this->y, $this->width, $this->height, $this->rx, $this->ry); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Shape.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Shape.php new file mode 100644 index 0000000..e9342a2 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Shape.php @@ -0,0 +1,62 @@ +document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyTransform($attributes); + } + + protected function after() + { + $surface = $this->document->getSurface(); + + if ($this->hasShape) { + $style = $surface->getStyle(); + + $fill = $style->fill && is_array($style->fill); + $stroke = $style->stroke && is_array($style->stroke); + + if ($fill) { + if ($stroke) { + $surface->fillStroke(false); + } else { +// if (is_string($style->fill)) { +// /** @var LinearGradient|RadialGradient $gradient */ +// $gradient = $this->getDocument()->getDef($style->fill); +// +// var_dump($gradient->getStops()); +// } + + $surface->fill(); + } + } + elseif ($stroke) { + $surface->stroke(false); + } + else { + $surface->endPath(); + } + } + + $surface->restore(); + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Stop.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Stop.php new file mode 100644 index 0000000..b7414f9 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Stop.php @@ -0,0 +1,16 @@ +text); + $this->document->appendStyleSheet($parser->parse()); + } + + public function appendText($text) + { + $this->text .= $text; + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Symbol.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Symbol.php new file mode 100644 index 0000000..3668a44 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Symbol.php @@ -0,0 +1,33 @@ +document->getSurface(); + + $surface->save(); + + $style = $this->makeStyle($attributes); + + $this->setStyle($style); + $surface->setStyle($style); + + $this->applyViewbox($attributes); + $this->applyTransform($attributes); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } +} \ No newline at end of file diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/Text.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Text.php new file mode 100644 index 0000000..0433658 --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/Text.php @@ -0,0 +1,71 @@ +document->getHeight(); + $this->y = $height; + + if (isset($attributes['x'])) { + $width = $this->document->getWidth(); + $this->x = $this->convertSize($attributes['x'], $width); + } + if (isset($attributes['y'])) { + $this->y = $height - $this->convertSize($attributes['y'], $height); + } + + $this->document->getSurface()->transform(1, 0, 0, -1, 0, $height); + } + + public function end() + { + $surface = $this->document->getSurface(); + $x = $this->x; + $y = $this->y; + $style = $surface->getStyle(); + $surface->setFont($style->fontFamily, $style->fontStyle, $style->fontWeight); + + switch ($style->textAnchor) { + case "middle": + $width = $surface->measureText($this->text); + $x -= $width / 2; + break; + + case "end": + $width = $surface->measureText($this->text); + $x -= $width; + break; + } + + $surface->fillText($this->getText(), $x, $y); + } + + protected function after() + { + $this->document->getSurface()->restore(); + } + + public function appendText($text) + { + $this->text .= $text; + } + + public function getText() + { + return trim($this->text); + } +} diff --git a/vendor/dompdf/php-svg-lib/src/Svg/Tag/UseTag.php b/vendor/dompdf/php-svg-lib/src/Svg/Tag/UseTag.php new file mode 100644 index 0000000..66c460b --- /dev/null +++ b/vendor/dompdf/php-svg-lib/src/Svg/Tag/UseTag.php @@ -0,0 +1,131 @@ +instances++; + if ($this->instances > 1) { + //TODO: log circular reference error state + return; + } + + if (isset($attributes['x'])) { + $this->x = $attributes['x']; + } + if (isset($attributes['y'])) { + $this->y = $attributes['y']; + } + + if (isset($attributes['width'])) { + $this->width = $attributes['width']; + } + if (isset($attributes['height'])) { + $this->height = $attributes['height']; + } + + parent::before($attributes); + + $document = $this->getDocument(); + + $link = $attributes["href"] ?? $attributes["xlink:href"]; + $this->reference = $document->getDef($link); + + $surface = $document->getSurface(); + $surface->save(); + + $surface->translate($this->x, $this->y); + } + + protected function after() { + if ($this->instances > 0) { + return; + } + parent::after(); + $this->getDocument()->getSurface()->restore(); + } + + public function handle($attributes) + { + if ($this->instances > 1) { + //TODO: log circular reference error state + return; + } + + parent::handle($attributes); + + if (!$this->reference) { + return; + } + + $originalAttributes = array_merge($this->reference->attributes); + $originalStyle = $this->reference->getStyle(); + $mergedAttributes = $this->reference->attributes; + $attributesToNotMerge = ['x', 'y', 'width', 'height', 'href', 'xlink:href', 'id', 'style']; + foreach ($attributes as $attrKey => $attrVal) { + if (!in_array($attrKey, $attributesToNotMerge) && !isset($mergedAttributes[$attrKey])) { + $mergedAttributes[$attrKey] = $attrVal; + } + } + $mergedAttributes['style'] = ($attributes['style'] ?? '') . ';' . ($mergedAttributes['style'] ?? ''); + + $this->_handle($this->reference, $mergedAttributes); + + $this->reference->attributes = $originalAttributes; + if ($originalStyle !== null) { + $this->reference->setStyle($originalStyle); + } + } + + public function handleEnd() + { + $this->instances--; + if ($this->instances > 0) { + return; + } + + if ($this->reference) { + $this->_handleEnd($this->reference); + } + + parent::handleEnd(); + } + + private function _handle($tag, $attributes) { + $tag->handle($attributes); + foreach ($tag->children as $child) { + $originalAttributes = array_merge($child->attributes); + $originalStyle = $child->getStyle(); + $mergedAttributes = $child->attributes; + $mergedAttributes['style'] = ($attributes['style'] ?? '') . ';' . ($mergedAttributes['style'] ?? ''); + $this->_handle($child, $mergedAttributes); + $child->attributes = $originalAttributes; + if ($originalStyle !== null) { + $child->setStyle($originalStyle); + } + } + } + + private function _handleEnd($tag) { + foreach ($tag->children as $child) { + $this->_handleEnd($child); + } + $tag->handleEnd(); + } +} diff --git a/vendor/ezyang/htmlpurifier/CREDITS b/vendor/ezyang/htmlpurifier/CREDITS new file mode 100644 index 0000000..7921b45 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/CREDITS @@ -0,0 +1,9 @@ + +CREDITS + +Almost everything written by Edward Z. Yang (Ambush Commander). Lots of thanks +to the DevNetwork Community for their help (see docs/ref-devnetwork.html for +more details), Feyd especially (namely IPv6 and optimization). Thanks to RSnake +for letting me package his fantastic XSS cheatsheet for a smoketest. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/LICENSE b/vendor/ezyang/htmlpurifier/LICENSE new file mode 100644 index 0000000..8c88a20 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/README.md b/vendor/ezyang/htmlpurifier/README.md new file mode 100644 index 0000000..e6b7199 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/README.md @@ -0,0 +1,29 @@ +HTML Purifier [![Build Status](https://github.com/ezyang/htmlpurifier/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ezyang/htmlpurifier/actions/workflows/ci.yml) +============= + +HTML Purifier is an HTML filtering solution that uses a unique combination +of robust whitelists and aggressive parsing to ensure that not only are +XSS attacks thwarted, but the resulting HTML is standards compliant. + +HTML Purifier is oriented towards richly formatted documents from +untrusted sources that require CSS and a full tag-set. This library can +be configured to accept a more restrictive set of tags, but it won't be +as efficient as more bare-bones parsers. It will, however, do the job +right, which may be more important. + +Places to go: + +* See INSTALL for a quick installation guide +* See docs/ for developer-oriented documentation, code examples and + an in-depth installation guide. +* See WYSIWYG for information on editors like TinyMCE and FCKeditor + +HTML Purifier can be found on the web at: [http://htmlpurifier.org/](http://htmlpurifier.org/) + +## Installation + +Package available on [Composer](https://packagist.org/packages/ezyang/htmlpurifier). + +If you're using Composer to manage dependencies, you can use + + $ composer require ezyang/htmlpurifier diff --git a/vendor/ezyang/htmlpurifier/VERSION b/vendor/ezyang/htmlpurifier/VERSION new file mode 100644 index 0000000..35e3d1b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/VERSION @@ -0,0 +1 @@ +4.19.0 \ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/composer.json b/vendor/ezyang/htmlpurifier/composer.json new file mode 100644 index 0000000..cfb7151 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/composer.json @@ -0,0 +1,45 @@ +{ + "name": "ezyang/htmlpurifier", + "description": "Standards compliant HTML filter written in PHP", + "type": "library", + "keywords": ["html"], + "homepage": "http://htmlpurifier.org/", + "license": "LGPL-2.1-or-later", + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "autoload": { + "psr-0": { "HTMLPurifier": "library/" }, + "files": ["library/HTMLPurifier.composer.php"], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-tidy": "Used for pretty-printing HTML" + }, + "config": { + "sort-packages": true + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/ezyang/simpletest.git", + "no-api": true + } + ] +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php new file mode 100644 index 0000000..1960c39 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php @@ -0,0 +1,11 @@ +purify($html, $config); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php new file mode 100644 index 0000000..1f99a4a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php @@ -0,0 +1,236 @@ + $attributes) { + $allowed_elements[$element] = true; + foreach ($attributes as $attribute => $x) { + $allowed_attributes["$element.$attribute"] = true; + } + } + $config->set('HTML.AllowedElements', $allowed_elements); + $config->set('HTML.AllowedAttributes', $allowed_attributes); + if ($allowed_protocols !== null) { + $config->set('URI.AllowedSchemes', $allowed_protocols); + } + $purifier = new HTMLPurifier($config); + return $purifier->purify($string); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php new file mode 100644 index 0000000..39b1b65 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php @@ -0,0 +1,11 @@ +config = HTMLPurifier_Config::create($config); + $this->strategy = new HTMLPurifier_Strategy_Core(); + } + + /** + * Adds a filter to process the output. First come first serve + * + * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object + */ + public function addFilter($filter) + { + trigger_error( + 'HTMLPurifier->addFilter() is deprecated, use configuration directives' . + ' in the Filter namespace or Filter.Custom', + E_USER_WARNING + ); + $this->filters[] = $filter; + } + + /** + * Filters an HTML snippet/document to be XSS-free and standards-compliant. + * + * @param string $html String of HTML to purify + * @param HTMLPurifier_Config $config Config object for this operation, + * if omitted, defaults to the config object specified during this + * object's construction. The parameter can also be any type + * that HTMLPurifier_Config::create() supports. + * + * @return string Purified HTML + */ + public function purify($html, $config = null) + { + // :TODO: make the config merge in, instead of replace + $config = $config ? HTMLPurifier_Config::create($config) : $this->config; + + // implementation is partially environment dependant, partially + // configuration dependant + $lexer = HTMLPurifier_Lexer::create($config); + + $context = new HTMLPurifier_Context(); + + // setup HTML generator + $this->generator = new HTMLPurifier_Generator($config, $context); + $context->register('Generator', $this->generator); + + // set up global context variables + if ($config->get('Core.CollectErrors')) { + // may get moved out if other facilities use it + $language_factory = HTMLPurifier_LanguageFactory::instance(); + $language = $language_factory->create($config, $context); + $context->register('Locale', $language); + + $error_collector = new HTMLPurifier_ErrorCollector($context); + $context->register('ErrorCollector', $error_collector); + } + + // setup id_accumulator context, necessary due to the fact that + // AttrValidator can be called from many places + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + + $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); + + // setup filters + $filter_flags = $config->getBatch('Filter'); + $custom_filters = $filter_flags['Custom']; + unset($filter_flags['Custom']); + $filters = array(); + foreach ($filter_flags as $filter => $flag) { + if (!$flag) { + continue; + } + if (strpos($filter, '.') !== false) { + continue; + } + $class = "HTMLPurifier_Filter_$filter"; + $filters[] = new $class; + } + foreach ($custom_filters as $filter) { + // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat + $filters[] = $filter; + } + $filters = array_merge($filters, $this->filters); + // maybe prepare(), but later + + for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { + $html = $filters[$i]->preFilter($html, $config, $context); + } + + // purified HTML + $html = + $this->generator->generateFromTokens( + // list of tokens + $this->strategy->execute( + // list of un-purified tokens + $lexer->tokenizeHTML( + // un-purified HTML + $html, + $config, + $context + ), + $config, + $context + ) + ); + + for ($i = $filter_size - 1; $i >= 0; $i--) { + $html = $filters[$i]->postFilter($html, $config, $context); + } + + $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); + $this->context =& $context; + return $html; + } + + /** + * Filters an array of HTML snippets + * + * @param string[] $array_of_html Array of html snippets + * @param HTMLPurifier_Config $config Optional config object for this operation. + * See HTMLPurifier::purify() for more details. + * + * @return string[] Array of purified HTML + */ + public function purifyArray($array_of_html, $config = null) + { + $context_array = array(); + $array = array(); + foreach($array_of_html as $key=>$value){ + if (is_array($value)) { + $array[$key] = $this->purifyArray($value, $config); + } else { + $array[$key] = $this->purify($value, $config); + } + $context_array[$key] = $this->context; + } + $this->context = $context_array; + return $array; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + */ + public static function instance($prototype = null) + { + if (!self::$instance || $prototype) { + if ($prototype instanceof HTMLPurifier) { + self::$instance = $prototype; + } elseif ($prototype) { + self::$instance = new HTMLPurifier($prototype); + } else { + self::$instance = new HTMLPurifier(); + } + } + return self::$instance; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + * @note Backwards compatibility, see instance() + */ + public static function getInstance($prototype = null) + { + return HTMLPurifier::instance($prototype); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php new file mode 100644 index 0000000..8a417d2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php @@ -0,0 +1,230 @@ +getHTMLDefinition(); + $parent = new HTMLPurifier_Token_Start($definition->info_parent); + $stack = array($parent->toNode()); + foreach ($tokens as $token) { + $token->skip = null; // [MUT] + $token->carryover = null; // [MUT] + if ($token instanceof HTMLPurifier_Token_End) { + $token->start = null; // [MUT] + $r = array_pop($stack); + //assert($r->name === $token->name); + //assert(empty($token->attr)); + $r->endCol = $token->col; + $r->endLine = $token->line; + $r->endArmor = $token->armor; + continue; + } + $node = $token->toNode(); + $stack[count($stack)-1]->children[] = $node; + if ($token instanceof HTMLPurifier_Token_Start) { + $stack[] = $node; + } + } + //assert(count($stack) == 1); + return $stack[0]; + } + + public static function flatten($node, $config, $context) { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingTokens = array(); + $tokens = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + list($start, $end) = $node->toTokenPair(); + if ($level > 0) { + $tokens[] = $start; + } + if ($end !== NULL) { + $closingTokens[$level][] = $end; + } + if ($node instanceof HTMLPurifier_Node_Element) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->children as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingTokens[$level])) { + while ($token = array_pop($closingTokens[$level])) { + $tokens[] = $token; + } + } + } while ($level > 0); + return $tokens; + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php new file mode 100644 index 0000000..c7b17cf --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php @@ -0,0 +1,148 @@ +doConstruct($attr_types, $modules); + } + + public function doConstruct($attr_types, $modules) + { + // load extensions from the modules + foreach ($modules as $module) { + foreach ($module->attr_collections as $coll_i => $coll) { + if (!isset($this->info[$coll_i])) { + $this->info[$coll_i] = array(); + } + foreach ($coll as $attr_i => $attr) { + if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { + // merge in includes + $this->info[$coll_i][$attr_i] = array_merge( + $this->info[$coll_i][$attr_i], + $attr + ); + continue; + } + $this->info[$coll_i][$attr_i] = $attr; + } + } + } + // perform internal expansions and inclusions + foreach ($this->info as $name => $attr) { + // merge attribute collections that include others + $this->performInclusions($this->info[$name]); + // replace string identifiers with actual attribute objects + $this->expandIdentifiers($this->info[$name], $attr_types); + } + } + + /** + * Takes a reference to an attribute associative array and performs + * all inclusions specified by the zero index. + * @param array &$attr Reference to attribute array + */ + public function performInclusions(&$attr) + { + if (!isset($attr[0])) { + return; + } + $merge = $attr[0]; + $seen = array(); // recursion guard + // loop through all the inclusions + for ($i = 0; isset($merge[$i]); $i++) { + if (isset($seen[$merge[$i]])) { + continue; + } + $seen[$merge[$i]] = true; + // foreach attribute of the inclusion, copy it over + if (!isset($this->info[$merge[$i]])) { + continue; + } + foreach ($this->info[$merge[$i]] as $key => $value) { + if (isset($attr[$key])) { + continue; + } // also catches more inclusions + $attr[$key] = $value; + } + if (isset($this->info[$merge[$i]][0])) { + // recursion + $merge = array_merge($merge, $this->info[$merge[$i]][0]); + } + } + unset($attr[0]); + } + + /** + * Expands all string identifiers in an attribute array by replacing + * them with the appropriate values inside HTMLPurifier_AttrTypes + * @param array &$attr Reference to attribute array + * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance + */ + public function expandIdentifiers(&$attr, $attr_types) + { + // because foreach will process new elements we add, make sure we + // skip duplicates + $processed = array(); + + foreach ($attr as $def_i => $def) { + // skip inclusions + if ($def_i === 0) { + continue; + } + + if (isset($processed[$def_i])) { + continue; + } + + // determine whether or not attribute is required + if ($required = (strpos($def_i, '*') !== false)) { + // rename the definition + unset($attr[$def_i]); + $def_i = trim($def_i, '*'); + $attr[$def_i] = $def; + } + + $processed[$def_i] = true; + + // if we've already got a literal object, move on + if (is_object($def)) { + // preserve previous required + $attr[$def_i]->required = ($required || $attr[$def_i]->required); + continue; + } + + if ($def === false) { + unset($attr[$def_i]); + continue; + } + + if ($t = $attr_types->get($def)) { + $attr[$def_i] = $t; + $attr[$def_i]->required = $required; + } else { + unset($attr[$def_i]); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php new file mode 100644 index 0000000..739646f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php @@ -0,0 +1,144 @@ + by removing + * leading and trailing whitespace, ignoring line feeds, and replacing + * carriage returns and tabs with spaces. While most useful for HTML + * attributes specified as CDATA, it can also be applied to most CSS + * values. + * + * @note This method is not entirely standards compliant, as trim() removes + * more types of whitespace than specified in the spec. In practice, + * this is rarely a problem, as those extra characters usually have + * already been removed by HTMLPurifier_Encoder. + * + * @warning This processing is inconsistent with XML's whitespace handling + * as specified by section 3.3.3 and referenced XHTML 1.0 section + * 4.7. However, note that we are NOT necessarily + * parsing XML, thus, this behavior may still be correct. We + * assume that newlines have been normalized. + */ + public function parseCDATA($string) + { + $string = trim($string); + $string = str_replace(array("\n", "\t", "\r"), ' ', $string); + return $string; + } + + /** + * Factory method for creating this class from a string. + * @param string $string String construction info + * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string + */ + public function make($string) + { + // default implementation, return a flyweight of this object. + // If $string has an effect on the returned object (i.e. you + // need to overload this method), it is best + // to clone or instantiate new copies. (Instantiation is safer.) + return $this; + } + + /** + * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work + * properly. THIS IS A HACK! + * @param string $string a CSS colour definition + * @return string + */ + protected function mungeRgb($string) + { + $p = '\s*(\d+(\.\d+)?([%]?))\s*'; + + if (preg_match('/(rgba|hsla)\(/', $string)) { + return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); + } + + return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); + } + + /** + * Parses a possibly escaped CSS string and returns the "pure" + * version of it. + */ + protected function expandCSSEscape($string) + { + // flexibly parse it + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] === '\\') { + $i++; + if ($i >= $c) { + $ret .= '\\'; + break; + } + if (ctype_xdigit($string[$i])) { + $code = $string[$i]; + for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { + if (!ctype_xdigit($string[$i])) { + break; + } + $code .= $string[$i]; + } + // We have to be extremely careful when adding + // new characters, to make sure we're not breaking + // the encoding. + $char = HTMLPurifier_Encoder::unichr(hexdec($code)); + if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { + continue; + } + $ret .= $char; + if ($i < $c && trim($string[$i]) !== '') { + $i--; + } + continue; + } + if ($string[$i] === "\n") { + continue; + } + } + $ret .= $string[$i]; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php new file mode 100644 index 0000000..af6b8a0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php @@ -0,0 +1,140 @@ +parseCDATA($css); + + $definition = $config->getCSSDefinition(); + $allow_duplicates = $config->get("CSS.AllowDuplicates"); + + $universal_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'initial', + 'inherit', + 'unset', + ) + ); + + // According to the CSS2.1 spec, the places where a + // non-delimiting semicolon can appear are in strings + // escape sequences. So here is some dumb hack to + // handle quotes. + $len = strlen($css); + $accum = ""; + $declarations = array(); + $quoted = false; + for ($i = 0; $i < $len; $i++) { + $c = strcspn($css, ";'\"", $i); + $accum .= substr($css, $i, $c); + $i += $c; + if ($i == $len) break; + $d = $css[$i]; + if ($quoted) { + $accum .= $d; + if ($d == $quoted) { + $quoted = false; + } + } else { + if ($d == ";") { + $declarations[] = $accum; + $accum = ""; + } else { + $accum .= $d; + $quoted = $d; + } + } + } + if ($accum != "") $declarations[] = $accum; + + $propvalues = array(); + $new_declarations = ''; + + /** + * Name of the current CSS property being validated. + */ + $property = false; + $context->register('CurrentCSSProperty', $property); + + foreach ($declarations as $declaration) { + if (!$declaration) { + continue; + } + if (!strpos($declaration, ':')) { + continue; + } + list($property, $value) = explode(':', $declaration, 2); + $property = trim($property); + $value = trim($value); + $ok = false; + do { + if (isset($definition->info[$property])) { + $ok = true; + break; + } + if (ctype_lower($property)) { + break; + } + $property = strtolower($property); + if (isset($definition->info[$property])) { + $ok = true; + break; + } + } while (0); + if (!$ok) { + continue; + } + $result = $universal_attrdef->validate($value, $config, $context); + if ($result === false) { + $result = $definition->info[$property]->validate( + $value, + $config, + $context + ); + } + if ($result === false) { + continue; + } + if ($allow_duplicates) { + $new_declarations .= "$property:$result;"; + } else { + $propvalues[$property] = $result; + } + } + + $context->destroy('CurrentCSSProperty'); + + // procedure does not write the new CSS simultaneously, so it's + // slightly inefficient, but it's the only way of getting rid of + // duplicates. Perhaps config to optimize it, but not now. + + foreach ($propvalues as $prop => $value) { + $new_declarations .= "$prop:$value;"; + } + + return $new_declarations ? $new_declarations : false; + + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php new file mode 100644 index 0000000..af2b83d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php @@ -0,0 +1,34 @@ + 1.0) { + $result = '1'; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php new file mode 100644 index 0000000..28c4988 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php @@ -0,0 +1,113 @@ +getCSSDefinition(); + $this->info['background-color'] = $def->info['background-color']; + $this->info['background-image'] = $def->info['background-image']; + $this->info['background-repeat'] = $def->info['background-repeat']; + $this->info['background-attachment'] = $def->info['background-attachment']; + $this->info['background-position'] = $def->info['background-position']; + $this->info['background-size'] = $def->info['background-size']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // munge rgb() decl if necessary + $string = $this->mungeRgb($string); + + // assumes URI doesn't have spaces in it + $bits = explode(' ', $string); // bits to process + + $caught = array(); + $caught['color'] = false; + $caught['image'] = false; + $caught['repeat'] = false; + $caught['attachment'] = false; + $caught['position'] = false; + $caught['size'] = false; + + $i = 0; // number of catches + + foreach ($bits as $bit) { + if ($bit === '') { + continue; + } + foreach ($caught as $key => $status) { + if ($key != 'position') { + if ($status !== false) { + continue; + } + $r = $this->info['background-' . $key]->validate($bit, $config, $context); + } else { + $r = $bit; + } + if ($r === false) { + continue; + } + if ($key == 'position') { + if ($caught[$key] === false) { + $caught[$key] = ''; + } + $caught[$key] .= $r . ' '; + } else { + $caught[$key] = $r; + } + $i++; + break; + } + } + + if (!$i) { + return false; + } + if ($caught['position'] !== false) { + $caught['position'] = $this->info['background-position']-> + validate($caught['position'], $config, $context); + } + + $ret = array(); + foreach ($caught as $value) { + if ($value === false) { + continue; + } + $ret[] = $value; + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php new file mode 100644 index 0000000..4580ef5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php @@ -0,0 +1,157 @@ + | | left | center | right + ] + [ + | | top | center | bottom + ]? + ] | + [ // this signifies that the vertical and horizontal adjectives + // can be arbitrarily ordered, however, there can only be two, + // one of each, or none at all + [ + left | center | right + ] || + [ + top | center | bottom + ] + ] + top, left = 0% + center, (none) = 50% + bottom, right = 100% +*/ + +/* QuirksMode says: + keyword + length/percentage must be ordered correctly, as per W3C + + Internet Explorer and Opera, however, support arbitrary ordering. We + should fix it up. + + Minor issue though, not strictly necessary. +*/ + +// control freaks may appreciate the ability to convert these to +// percentages or something, but it's not necessary + +/** + * Validates the value of background-position. + */ +class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef +{ + + /** + * @type HTMLPurifier_AttrDef_CSS_Length + */ + protected $length; + + /** + * @type HTMLPurifier_AttrDef_CSS_Percentage + */ + protected $percentage; + + public function __construct() + { + $this->length = new HTMLPurifier_AttrDef_CSS_Length(); + $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + $bits = explode(' ', $string); + + $keywords = array(); + $keywords['h'] = false; // left, right + $keywords['v'] = false; // top, bottom + $keywords['ch'] = false; // center (first word) + $keywords['cv'] = false; // center (second word) + $measures = array(); + + $i = 0; + + $lookup = array( + 'top' => 'v', + 'bottom' => 'v', + 'left' => 'h', + 'right' => 'h', + 'center' => 'c' + ); + + foreach ($bits as $bit) { + if ($bit === '') { + continue; + } + + // test for keyword + $lbit = ctype_lower($bit) ? $bit : strtolower($bit); + if (isset($lookup[$lbit])) { + $status = $lookup[$lbit]; + if ($status == 'c') { + if ($i == 0) { + $status = 'ch'; + } else { + $status = 'cv'; + } + } + $keywords[$status] = $lbit; + $i++; + } + + // test for length + $r = $this->length->validate($bit, $config, $context); + if ($r !== false) { + $measures[] = $r; + $i++; + } + + // test for percentage + $r = $this->percentage->validate($bit, $config, $context); + if ($r !== false) { + $measures[] = $r; + $i++; + } + } + + if (!$i) { + return false; + } // no valid values were caught + + $ret = array(); + + // first keyword + if ($keywords['h']) { + $ret[] = $keywords['h']; + } elseif ($keywords['ch']) { + $ret[] = $keywords['ch']; + $keywords['cv'] = false; // prevent re-use: center = center center + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if ($keywords['v']) { + $ret[] = $keywords['v']; + } elseif ($keywords['cv']) { + $ret[] = $keywords['cv']; + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php new file mode 100644 index 0000000..16243ba --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php @@ -0,0 +1,56 @@ +getCSSDefinition(); + $this->info['border-width'] = $def->info['border-width']; + $this->info['border-style'] = $def->info['border-style']; + $this->info['border-top-color'] = $def->info['border-top-color']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + $string = $this->mungeRgb($string); + $bits = explode(' ', $string); + $done = array(); // segments we've finished + $ret = ''; // return value + foreach ($bits as $bit) { + foreach ($this->info as $propname => $validator) { + if (isset($done[$propname])) { + continue; + } + $r = $validator->validate($bit, $config, $context); + if ($r !== false) { + $ret .= $r . ' '; + $done[$propname] = true; + break; + } + } + } + return rtrim($ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php new file mode 100644 index 0000000..d7287a0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php @@ -0,0 +1,161 @@ +alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param string $color + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($color, $config, $context) + { + static $colors = null; + if ($colors === null) { + $colors = $config->get('Core.ColorKeywords'); + } + + $color = trim($color); + if ($color === '') { + return false; + } + + $lower = strtolower($color); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + + if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) { + $length = strlen($color); + if (strpos($color, ')') !== $length - 1) { + return false; + } + + // get used function : rgb, rgba, hsl or hsla + $function = $matches[1]; + + $parameters_size = 3; + $alpha_channel = false; + if (substr($function, -1) === 'a') { + $parameters_size = 4; + $alpha_channel = true; + } + + /* + * Allowed types for values : + * parameter_position => [type => max_value] + */ + $allowed_types = array( + 1 => array('percentage' => 100, 'integer' => 255), + 2 => array('percentage' => 100, 'integer' => 255), + 3 => array('percentage' => 100, 'integer' => 255), + ); + $allow_different_types = false; + + if (strpos($function, 'hsl') !== false) { + $allowed_types = array( + 1 => array('integer' => 360), + 2 => array('percentage' => 100), + 3 => array('percentage' => 100), + ); + $allow_different_types = true; + } + + $values = trim(str_replace($function, '', $color), ' ()'); + + $parts = explode(',', $values); + if (count($parts) !== $parameters_size) { + return false; + } + + $type = false; + $new_parts = array(); + $i = 0; + + foreach ($parts as $part) { + $i++; + $part = trim($part); + + if ($part === '') { + return false; + } + + // different check for alpha channel + if ($alpha_channel === true && $i === count($parts)) { + $result = $this->alpha->validate($part, $config, $context); + + if ($result === false) { + return false; + } + + $new_parts[] = (string)$result; + continue; + } + + if (substr($part, -1) === '%') { + $current_type = 'percentage'; + } else { + $current_type = 'integer'; + } + + if (!array_key_exists($current_type, $allowed_types[$i])) { + return false; + } + + if (!$type) { + $type = $current_type; + } + + if ($allow_different_types === false && $type != $current_type) { + return false; + } + + $max_value = $allowed_types[$i][$current_type]; + + if ($current_type == 'integer') { + // Return value between range 0 -> $max_value + $new_parts[] = (int)max(min($part, $max_value), 0); + } elseif ($current_type == 'percentage') { + $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%'; + } + } + + $new_values = implode(',', $new_parts); + + $color = $function . '(' . $new_values . ')'; + } else { + // hexadecimal handling + if ($color[0] === '#') { + $hex = substr($color, 1); + } else { + $hex = $color; + $color = '#' . $color; + } + $length = strlen($hex); + if ($length !== 3 && $length !== 6) { + return false; + } + if (!ctype_xdigit($hex)) { + return false; + } + } + return $color; + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php new file mode 100644 index 0000000..9c17505 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php @@ -0,0 +1,48 @@ +defs = $defs; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + foreach ($this->defs as $i => $def) { + $result = $this->defs[$i]->validate($string, $config, $context); + if ($result !== false) { + return $result; + } + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php new file mode 100644 index 0000000..9d77cc9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php @@ -0,0 +1,44 @@ +def = $def; + $this->element = $element; + } + + /** + * Checks if CurrentToken is set and equal to $this->element + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $token = $context->get('CurrentToken', true); + if ($token && $token->name == $this->element) { + return false; + } + return $this->def->validate($string, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php new file mode 100644 index 0000000..bde4c33 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php @@ -0,0 +1,77 @@ +intValidator = new HTMLPurifier_AttrDef_Integer(); + } + + /** + * @param string $value + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($value, $config, $context) + { + $value = $this->parseCDATA($value); + if ($value === 'none') { + return $value; + } + // if we looped this we could support multiple filters + $function_length = strcspn($value, '('); + $function = trim(substr($value, 0, $function_length)); + if ($function !== 'alpha' && + $function !== 'Alpha' && + $function !== 'progid:DXImageTransform.Microsoft.Alpha' + ) { + return false; + } + $cursor = $function_length + 1; + $parameters_length = strcspn($value, ')', $cursor); + $parameters = substr($value, $cursor, $parameters_length); + $params = explode(',', $parameters); + $ret_params = array(); + $lookup = array(); + foreach ($params as $param) { + list($key, $value) = explode('=', $param); + $key = trim($key); + $value = trim($value); + if (isset($lookup[$key])) { + continue; + } + if ($key !== 'opacity') { + continue; + } + $value = $this->intValidator->validate($value, $config, $context); + if ($value === false) { + continue; + } + $int = (int)$value; + if ($int > 100) { + $value = '100'; + } + if ($int < 0) { + $value = '0'; + } + $ret_params[] = "$key=$value"; + $lookup[$key] = true; + } + $ret_parameters = implode(',', $ret_params); + $ret_function = "$function($ret_parameters)"; + return $ret_function; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php new file mode 100644 index 0000000..579b97e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php @@ -0,0 +1,176 @@ +getCSSDefinition(); + $this->info['font-style'] = $def->info['font-style']; + $this->info['font-variant'] = $def->info['font-variant']; + $this->info['font-weight'] = $def->info['font-weight']; + $this->info['font-size'] = $def->info['font-size']; + $this->info['line-height'] = $def->info['line-height']; + $this->info['font-family'] = $def->info['font-family']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $system_fonts = array( + 'caption' => true, + 'icon' => true, + 'menu' => true, + 'message-box' => true, + 'small-caption' => true, + 'status-bar' => true + ); + + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // check if it's one of the keywords + $lowercase_string = strtolower($string); + if (isset($system_fonts[$lowercase_string])) { + return $lowercase_string; + } + + $bits = explode(' ', $string); // bits to process + $stage = 0; // this indicates what we're looking for + $caught = array(); // which stage 0 properties have we caught? + $stage_1 = array('font-style', 'font-variant', 'font-weight'); + $final = ''; // output + + for ($i = 0, $size = count($bits); $i < $size; $i++) { + if ($bits[$i] === '') { + continue; + } + switch ($stage) { + case 0: // attempting to catch font-style, font-variant or font-weight + foreach ($stage_1 as $validator_name) { + if (isset($caught[$validator_name])) { + continue; + } + $r = $this->info[$validator_name]->validate( + $bits[$i], + $config, + $context + ); + if ($r !== false) { + $final .= $r . ' '; + $caught[$validator_name] = true; + break; + } + } + // all three caught, continue on + if (count($caught) >= 3) { + $stage = 1; + } + if ($r !== false) { + break; + } + case 1: // attempting to catch font-size and perhaps line-height + $found_slash = false; + if (strpos($bits[$i], '/') !== false) { + list($font_size, $line_height) = + explode('/', $bits[$i]); + if ($line_height === '') { + // ooh, there's a space after the slash! + $line_height = false; + $found_slash = true; + } + } else { + $font_size = $bits[$i]; + $line_height = false; + } + $r = $this->info['font-size']->validate( + $font_size, + $config, + $context + ); + if ($r !== false) { + $final .= $r; + // attempt to catch line-height + if ($line_height === false) { + // we need to scroll forward + for ($j = $i + 1; $j < $size; $j++) { + if ($bits[$j] === '') { + continue; + } + if ($bits[$j] === '/') { + if ($found_slash) { + return false; + } else { + $found_slash = true; + continue; + } + } + $line_height = $bits[$j]; + break; + } + } else { + // slash already found + $found_slash = true; + $j = $i; + } + if ($found_slash) { + $i = $j; + $r = $this->info['line-height']->validate( + $line_height, + $config, + $context + ); + if ($r !== false) { + $final .= '/' . $r; + } + } + $final .= ' '; + $stage = 2; + break; + } + return false; + case 2: // attempting to catch font-family + $font_family = + implode(' ', array_slice($bits, $i, $size - $i)); + $r = $this->info['font-family']->validate( + $font_family, + $config, + $context + ); + if ($r !== false) { + $final .= $r . ' '; + // processing completed successfully + return rtrim($final); + } + return false; + } + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php new file mode 100644 index 0000000..799166b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -0,0 +1,217 @@ +mask = array_reduce($c, function ($carry, $value) { + return $carry . $value; + }, '_- '); + + /* + PHP's internal strcspn implementation is + O(length of string * length of mask), making it inefficient + for large masks. However, it's still faster than + preg_match 8) + for (p = s1;;) { + spanp = s2; + do { + if (*spanp == c || p == s1_end) { + return p - s1; + } + } while (spanp++ < (s2_end - 1)); + c = *++p; + } + */ + // possible optimization: invert the mask. + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $generic_names = array( + 'serif' => true, + 'sans-serif' => true, + 'monospace' => true, + 'fantasy' => true, + 'cursive' => true + ); + $allowed_fonts = $config->get('CSS.AllowedFonts'); + + // assume that no font names contain commas in them + $fonts = explode(',', $string); + $final = ''; + foreach ($fonts as $font) { + $font = trim($font); + if ($font === '') { + continue; + } + // match a generic name + if (isset($generic_names[$font])) { + if ($allowed_fonts === null || isset($allowed_fonts[$font])) { + $final .= $font . ', '; + } + continue; + } + // match a quoted name + if ($font[0] === '"' || $font[0] === "'") { + $length = strlen($font); + if ($length <= 2) { + continue; + } + $quote = $font[0]; + if ($font[$length - 1] !== $quote) { + continue; + } + $font = substr($font, 1, $length - 2); + } + + $font = $this->expandCSSEscape($font); + + // $font is a pure representation of the font name + + if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { + continue; + } + + if (ctype_alnum($font) && $font !== '') { + // very simple font, allow it in unharmed + $final .= $font . ', '; + continue; + } + + // bugger out on whitespace. form feed (0C) really + // shouldn't show up regardless + $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); + + // Here, there are various classes of characters which need + // to be treated differently: + // - Alphanumeric characters are essentially safe. We + // handled these above. + // - Spaces require quoting, though most parsers will do + // the right thing if there aren't any characters that + // can be misinterpreted + // - Dashes rarely occur, but they fairly unproblematic + // for parsing/rendering purposes. + // The above characters cover the majority of Western font + // names. + // - Arbitrary Unicode characters not in ASCII. Because + // most parsers give little thought to Unicode, treatment + // of these codepoints is basically uniform, even for + // punctuation-like codepoints. These characters can + // show up in non-Western pages and are supported by most + // major browsers, for example: "MS 明朝" is a + // legitimate font-name + // . See + // the CSS3 spec for more examples: + // + // You can see live samples of these on the Internet: + // + // However, most of these fonts have ASCII equivalents: + // for example, 'MS Mincho', and it's considered + // professional to use ASCII font names instead of + // Unicode font names. Thanks Takeshi Terada for + // providing this information. + // The following characters, to my knowledge, have not been + // used to name font names. + // - Single quote. While theoretically you might find a + // font name that has a single quote in its name (serving + // as an apostrophe, e.g. Dave's Scribble), I haven't + // been able to find any actual examples of this. + // Internet Explorer's cssText translation (which I + // believe is invoked by innerHTML) normalizes any + // quoting to single quotes, and fails to escape single + // quotes. (Note that this is not IE's behavior for all + // CSS properties, just some sort of special casing for + // font-family). So a single quote *cannot* be used + // safely in the font-family context if there will be an + // innerHTML/cssText translation. Note that Firefox 3.x + // does this too. + // - Double quote. In IE, these get normalized to + // single-quotes, no matter what the encoding. (Fun + // fact, in IE8, the 'content' CSS property gained + // support, where they special cased to preserve encoded + // double quotes, but still translate unadorned double + // quotes into single quotes.) So, because their + // fixpoint behavior is identical to single quotes, they + // cannot be allowed either. Firefox 3.x displays + // single-quote style behavior. + // - Backslashes are reduced by one (so \\ -> \) every + // iteration, so they cannot be used safely. This shows + // up in IE7, IE8 and FF3 + // - Semicolons, commas and backticks are handled properly. + // - The rest of the ASCII punctuation is handled properly. + // We haven't checked what browsers do to unadorned + // versions, but this is not important as long as the + // browser doesn't /remove/ surrounding quotes (as IE does + // for HTML). + // + // With these results in hand, we conclude that there are + // various levels of safety: + // - Paranoid: alphanumeric, spaces and dashes(?) + // - International: Paranoid + non-ASCII Unicode + // - Edgy: Everything except quotes, backslashes + // - NoJS: Standards compliance, e.g. sod IE. Note that + // with some judicious character escaping (since certain + // types of escaping doesn't work) this is theoretically + // OK as long as innerHTML/cssText is not called. + // We believe that international is a reasonable default + // (that we will implement now), and once we do more + // extensive research, we may feel comfortable with dropping + // it down to edgy. + + // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of + // str(c)spn assumes that the string was already well formed + // Unicode (which of course it is). + if (strspn($font, $this->mask) !== strlen($font)) { + continue; + } + + // Historical: + // In the absence of innerHTML/cssText, these ugly + // transforms don't pose a security risk (as \\ and \" + // might--these escapes are not supported by most browsers). + // We could try to be clever and use single-quote wrapping + // when there is a double quote present, but I have chosen + // not to implement that. (NOTE: you can reduce the amount + // of escapes by one depending on what quoting style you use) + // $font = str_replace('\\', '\\5C ', $font); + // $font = str_replace('"', '\\22 ', $font); + // $font = str_replace("'", '\\27 ', $font); + + // font possibly with spaces, requires quoting + $final .= "'$font', "; + } + $final = rtrim($final, ', '); + if ($final === '') { + return false; + } + return $final; + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php new file mode 100644 index 0000000..973002c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php @@ -0,0 +1,32 @@ +def = $def; + $this->allow = $allow; + } + + /** + * Intercepts and removes !important if necessary + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // test for ! and important tokens + $string = trim($string); + $is_important = false; + // :TODO: optimization: test directly for !important and ! important + if (strlen($string) >= 9 && substr($string, -9) === 'important') { + $temp = rtrim(substr($string, 0, -9)); + // use a temp, because we might want to restore important + if (strlen($temp) >= 1 && substr($temp, -1) === '!') { + $string = rtrim(substr($temp, 0, -1)); + $is_important = true; + } + } + $string = $this->def->validate($string, $config, $context); + if ($this->allow && $is_important) { + $string .= ' !important'; + } + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php new file mode 100644 index 0000000..f12453a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php @@ -0,0 +1,77 @@ +min = $min !== null ? HTMLPurifier_Length::make($min) : null; + $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + + // Optimizations + if ($string === '') { + return false; + } + if ($string === '0') { + return '0'; + } + if (strlen($string) === 1) { + return false; + } + + $length = HTMLPurifier_Length::make($string); + if (!$length->isValid()) { + return false; + } + + if ($this->min) { + $c = $length->compareTo($this->min); + if ($c === false) { + return false; + } + if ($c < 0) { + return false; + } + } + if ($this->max) { + $c = $length->compareTo($this->max); + if ($c === false) { + return false; + } + if ($c > 0) { + return false; + } + } + return $length->toString(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php new file mode 100644 index 0000000..e74d426 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php @@ -0,0 +1,112 @@ +getCSSDefinition(); + $this->info['list-style-type'] = $def->info['list-style-type']; + $this->info['list-style-position'] = $def->info['list-style-position']; + $this->info['list-style-image'] = $def->info['list-style-image']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // assumes URI doesn't have spaces in it + $bits = explode(' ', strtolower($string)); // bits to process + + $caught = array(); + $caught['type'] = false; + $caught['position'] = false; + $caught['image'] = false; + + $i = 0; // number of catches + $none = false; + + foreach ($bits as $bit) { + if ($i >= 3) { + return; + } // optimization bit + if ($bit === '') { + continue; + } + foreach ($caught as $key => $status) { + if ($status !== false) { + continue; + } + $r = $this->info['list-style-' . $key]->validate($bit, $config, $context); + if ($r === false) { + continue; + } + if ($r === 'none') { + if ($none) { + continue; + } else { + $none = true; + } + if ($key == 'image') { + continue; + } + } + $caught[$key] = $r; + $i++; + break; + } + } + + if (!$i) { + return false; + } + + $ret = array(); + + // construct type + if ($caught['type']) { + $ret[] = $caught['type']; + } + + // construct image + if ($caught['image']) { + $ret[] = $caught['image']; + } + + // construct position + if ($caught['position']) { + $ret[] = $caught['position']; + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php new file mode 100644 index 0000000..e707f87 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php @@ -0,0 +1,71 @@ +single = $single; + $this->max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->mungeRgb($this->parseCDATA($string)); + if ($string === '') { + return false; + } + $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n + $length = count($parts); + $final = ''; + for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) { + if (ctype_space($parts[$i])) { + continue; + } + $result = $this->single->validate($parts[$i], $config, $context); + if ($result !== false) { + $final .= $result . ' '; + $num++; + } + } + if ($final === '') { + return false; + } + return rtrim($final); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php new file mode 100644 index 0000000..ef49d20 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php @@ -0,0 +1,90 @@ +non_negative = $non_negative; + } + + /** + * @param string $number + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string|bool + * @warning Some contexts do not pass $config, $context. These + * variables should not be used without checking HTMLPurifier_Length + */ + public function validate($number, $config, $context) + { + $number = $this->parseCDATA($number); + + if ($number === '') { + return false; + } + if ($number === '0') { + return '0'; + } + + $sign = ''; + switch ($number[0]) { + case '-': + if ($this->non_negative) { + return false; + } + $sign = '-'; + case '+': + $number = substr($number, 1); + } + + if (ctype_digit($number)) { + $number = ltrim($number, '0'); + return $number ? $sign . $number : '0'; + } + + // Period is the only non-numeric character allowed + if (strpos($number, '.') === false) { + return false; + } + + list($left, $right) = explode('.', $number, 2); + + if ($left === '' && $right === '') { + return false; + } + if ($left !== '' && !ctype_digit($left)) { + return false; + } + + // Remove leading zeros until positive number or a zero stays left + if (ltrim($left, '0') != '') { + $left = ltrim($left, '0'); + } else { + $left = '0'; + } + + $right = rtrim($right, '0'); + + if ($right === '') { + return $left ? $sign . $left : '0'; + } elseif (!ctype_digit($right)) { + return false; + } + return $sign . $left . '.' . $right; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php new file mode 100644 index 0000000..f0f25c5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php @@ -0,0 +1,54 @@ +number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + + if ($string === '') { + return false; + } + $length = strlen($string); + if ($length === 1) { + return false; + } + if ($string[$length - 1] !== '%') { + return false; + } + + $number = substr($string, 0, $length - 1); + $number = $this->number_def->validate($number, $config, $context); + + if ($number === false) { + return false; + } + return "$number%"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php new file mode 100644 index 0000000..e08e2c4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php @@ -0,0 +1,46 @@ +parseCDATA($ratio); + + $parts = explode('/', $ratio, 2); + $length = count($parts); + + if ($length < 1 || $length > 2) { + return false; + } + + $num = new \HTMLPurifier_AttrDef_CSS_Number(); + + if ($length === 1) { + return $num->validate($parts[0], $config, $context); + } + + $num1 = $num->validate($parts[0], $config, $context); + $num2 = $num->validate($parts[1], $config, $context); + + if ($num1 === false || $num2 === false) { + return false; + } + + return $num1 . '/' . $num2; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php new file mode 100644 index 0000000..5fd4b7f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php @@ -0,0 +1,46 @@ + true, + 'overline' => true, + 'underline' => true, + ); + + $string = strtolower($this->parseCDATA($string)); + + if ($string === 'none') { + return $string; + } + + $parts = explode(' ', $string); + $final = ''; + foreach ($parts as $part) { + if (isset($allowed_values[$part])) { + $final .= $part . ' '; + } + } + $final = rtrim($final); + if ($final === '') { + return false; + } + return $final; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php new file mode 100644 index 0000000..6617aca --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php @@ -0,0 +1,77 @@ +parseCDATA($uri_string); + if (strpos($uri_string, 'url(') !== 0) { + return false; + } + $uri_string = substr($uri_string, 4); + if (strlen($uri_string) == 0) { + return false; + } + $new_length = strlen($uri_string) - 1; + if ($uri_string[$new_length] != ')') { + return false; + } + $uri = trim(substr($uri_string, 0, $new_length)); + + if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { + $quote = $uri[0]; + $new_length = strlen($uri) - 1; + if ($uri[$new_length] !== $quote) { + return false; + } + $uri = substr($uri, 1, $new_length - 1); + } + + $uri = $this->expandCSSEscape($uri); + + $result = parent::validate($uri, $config, $context); + + if ($result === false) { + return false; + } + + // extra sanity check; should have been done by URI + $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result); + + // suspicious characters are ()'; we're going to percent encode + // them for safety. + $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result); + + // there's an extra bug where ampersands lose their escaping on + // an innerHTML cycle, so a very unlucky query parameter could + // then change the meaning of the URL. Unfortunately, there's + // not much we can do about that... + return "url(\"$result\")"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php new file mode 100644 index 0000000..6698a00 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php @@ -0,0 +1,44 @@ +clone = $clone; + } + + /** + * @param string $v + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($v, $config, $context) + { + return $this->clone->validate($v, $config, $context); + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + return clone $this->clone; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php new file mode 100644 index 0000000..8abda7f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php @@ -0,0 +1,73 @@ +valid_values = array_flip($valid_values); + $this->case_sensitive = $case_sensitive; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if (!$this->case_sensitive) { + // we may want to do full case-insensitive libraries + $string = ctype_lower($string) ? $string : strtolower($string); + } + $result = isset($this->valid_values[$string]); + + return $result ? $string : false; + } + + /** + * @param string $string In form of comma-delimited list of case-insensitive + * valid values. Example: "foo,bar,baz". Prepend "s:" to make + * case sensitive + * @return HTMLPurifier_AttrDef_Enum + */ + public function make($string) + { + if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') { + $string = substr($string, 2); + $sensitive = true; + } else { + $sensitive = false; + } + $values = explode(',', $string); + return new HTMLPurifier_AttrDef_Enum($values, $sensitive); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php new file mode 100644 index 0000000..be3bbc8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php @@ -0,0 +1,48 @@ +name = $name; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + return $this->name; + } + + /** + * @param string $string Name of attribute + * @return HTMLPurifier_AttrDef_HTML_Bool + */ + public function make($string) + { + return new HTMLPurifier_AttrDef_HTML_Bool($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php new file mode 100644 index 0000000..d501348 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php @@ -0,0 +1,48 @@ +getDefinition('HTML')->doctype->name; + if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { + return parent::split($string, $config, $context); + } else { + return preg_split('/\s+/', $string); + } + } + + /** + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + $allowed = $config->get('Attr.AllowedClasses'); + $forbidden = $config->get('Attr.ForbiddenClasses'); + $ret = array(); + foreach ($tokens as $token) { + if (($allowed === null || isset($allowed[$token])) && + !isset($forbidden[$token]) && + // We need this O(n) check because of PHP's array + // implementation that casts -0 to 0. + !in_array($token, $ret, true) + ) { + $ret[] = $token; + } + } + return $ret; + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php new file mode 100644 index 0000000..946ebb7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php @@ -0,0 +1,51 @@ +get('Core.ColorKeywords'); + } + + $string = trim($string); + + if (empty($string)) { + return false; + } + $lower = strtolower($string); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + if ($string[0] === '#') { + $hex = substr($string, 1); + } else { + $hex = $string; + } + + $length = strlen($hex); + if ($length !== 3 && $length !== 6) { + return false; + } + if (!ctype_xdigit($hex)) { + return false; + } + if ($length === 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + return "#$hex"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ContentEditable.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ContentEditable.php new file mode 100644 index 0000000..5b03d3e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ContentEditable.php @@ -0,0 +1,16 @@ +get('HTML.Trusted')) { + $allowed = array('', 'true', 'false'); + } + + $enum = new HTMLPurifier_AttrDef_Enum($allowed); + + return $enum->validate($string, $config, $context); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php new file mode 100644 index 0000000..d79ba12 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php @@ -0,0 +1,38 @@ +valid_values === false) { + $this->valid_values = $config->get('Attr.AllowedFrameTargets'); + } + return parent::validate($string, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php new file mode 100644 index 0000000..4ba4561 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php @@ -0,0 +1,113 @@ +selector = $selector; + } + + /** + * @param string $id + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($id, $config, $context) + { + if (!$this->selector && !$config->get('Attr.EnableID')) { + return false; + } + + $id = trim($id); // trim it first + + if ($id === '') { + return false; + } + + $prefix = $config->get('Attr.IDPrefix'); + if ($prefix !== '') { + $prefix .= $config->get('Attr.IDPrefixLocal'); + // prevent re-appending the prefix + if (strpos($id, $prefix) !== 0) { + $id = $prefix . $id; + } + } elseif ($config->get('Attr.IDPrefixLocal') !== '') { + trigger_error( + '%Attr.IDPrefixLocal cannot be used unless ' . + '%Attr.IDPrefix is set', + E_USER_WARNING + ); + } + + if (!$this->selector) { + $id_accumulator =& $context->get('IDAccumulator'); + if (isset($id_accumulator->ids[$id])) { + return false; + } + } + + // we purposely avoid using regex, hopefully this is faster + + if ($config->get('Attr.ID.HTML5') === true) { + if (preg_match('/[\t\n\x0b\x0c ]/', $id)) { + return false; + } + } else { + if (ctype_alpha($id)) { + // OK + } else { + if (!ctype_alpha(@$id[0])) { + return false; + } + // primitive style of regexps, I suppose + $trim = trim( + $id, + 'A..Za..z0..9:-._' + ); + if ($trim !== '') { + return false; + } + } + } + + $regexp = $config->get('Attr.IDBlacklistRegexp'); + if ($regexp && preg_match($regexp, $id)) { + return false; + } + + if (!$this->selector) { + $id_accumulator->add($id); + } + + // if no change was made to the ID, return the result + // else, return the new id if stripping whitespace made it + // valid, or return false. + return $id; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php new file mode 100644 index 0000000..1c4006f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php @@ -0,0 +1,56 @@ + 100) { + return '100%'; + } + return ((string)$points) . '%'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php new file mode 100644 index 0000000..3decf0c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -0,0 +1,67 @@ + 'AllowedRel', + 'rev' => 'AllowedRev' + ); + if (!isset($configLookup[$name])) { + throw new Exception('Unrecognized attribute name for link relationship.'); + } + $this->name = $configLookup[$name]; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $allowed = $config->get('Attr.' . $this->name); + if (empty($allowed)) { + return false; + } + + $string = $this->parseCDATA($string); + $parts = explode(' ', $string); + + // lookup to prevent duplicates + $ret_lookup = array(); + foreach ($parts as $part) { + $part = strtolower(trim($part)); + if (!isset($allowed[$part])) { + continue; + } + $ret_lookup[$part] = true; + } + + if (empty($ret_lookup)) { + return false; + } + $string = implode(' ', array_keys($ret_lookup)); + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php new file mode 100644 index 0000000..bbb20f2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php @@ -0,0 +1,60 @@ +split($string, $config, $context); + $tokens = $this->filter($tokens, $config, $context); + if (empty($tokens)) { + return false; + } + return implode(' ', $tokens); + } + + /** + * Splits a space separated list of tokens into its constituent parts. + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function split($string, $config, $context) + { + // OPTIMIZABLE! + // do the preg_match, capture all subpatterns for reformulation + + // we don't support U+00A1 and up codepoints or + // escaping because I don't know how to do that with regexps + // and plus it would complicate optimization efforts (you never + // see that anyway). + $pattern = '/(?:(?<=\s)|\A)' . // look behind for space or string start + '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)' . + '(?:(?=\s)|\z)/'; // look ahead for space or string end + preg_match_all($pattern, $string, $matches); + return $matches[1]; + } + + /** + * Template method for removing certain tokens based on arbitrary criteria. + * @note If we wanted to be really functional, we'd do an array_filter + * with a callback. But... we're not. + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php new file mode 100644 index 0000000..a1d019e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php @@ -0,0 +1,76 @@ +max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if ($string === '0') { + return $string; + } + if ($string === '') { + return false; + } + $length = strlen($string); + if (substr($string, $length - 2) == 'px') { + $string = substr($string, 0, $length - 2); + } + if (!is_numeric($string)) { + return false; + } + $int = (int)$string; + + if ($int < 0) { + return '0'; + } + + // upper-bound value, extremely high values can + // crash operating systems, see + // WARNING, above link WILL crash you if you're using Windows + + if ($this->max !== null && $int > $this->max) { + return (string)$this->max; + } + return (string)$int; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + if ($string === '') { + $max = null; + } else { + $max = (int)$string; + } + $class = get_class($this); + return new $class($max); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php new file mode 100644 index 0000000..400e707 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php @@ -0,0 +1,91 @@ +negative = $negative; + $this->zero = $zero; + $this->positive = $positive; + } + + /** + * @param string $integer + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($integer, $config, $context) + { + $integer = $this->parseCDATA($integer); + if ($integer === '') { + return false; + } + + // we could possibly simply typecast it to integer, but there are + // certain fringe cases that must not return an integer. + + // clip leading sign + if ($this->negative && $integer[0] === '-') { + $digits = substr($integer, 1); + if ($digits === '0') { + $integer = '0'; + } // rm minus sign for zero + } elseif ($this->positive && $integer[0] === '+') { + $digits = $integer = substr($integer, 1); // rm unnecessary plus + } else { + $digits = $integer; + } + + // test if it's numeric + if (!ctype_digit($digits)) { + return false; + } + + // perform scope tests + if (!$this->zero && $integer == 0) { + return false; + } + if (!$this->positive && $integer > 0) { + return false; + } + if (!$this->negative && $integer < 0) { + return false; + } + + return $integer; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php new file mode 100644 index 0000000..2a55cea --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php @@ -0,0 +1,86 @@ + 8 || !ctype_alnum($subtags[1])) { + return $new_string; + } + if (!ctype_lower($subtags[1])) { + $subtags[1] = strtolower($subtags[1]); + } + + $new_string .= '-' . $subtags[1]; + if ($num_subtags == 2) { + return $new_string; + } + + // process all other subtags, index 2 and up + for ($i = 2; $i < $num_subtags; $i++) { + $length = strlen($subtags[$i]); + if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) { + return $new_string; + } + if (!ctype_lower($subtags[$i])) { + $subtags[$i] = strtolower($subtags[$i]); + } + $new_string .= '-' . $subtags[$i]; + } + return $new_string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php new file mode 100644 index 0000000..c7eb319 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php @@ -0,0 +1,53 @@ +tag = $tag; + $this->withTag = $with_tag; + $this->withoutTag = $without_tag; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $token = $context->get('CurrentToken', true); + if (!$token || $token->name !== $this->tag) { + return $this->withoutTag->validate($string, $config, $context); + } else { + return $this->withTag->validate($string, $config, $context); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php new file mode 100644 index 0000000..4553a4e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php @@ -0,0 +1,21 @@ +parseCDATA($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php new file mode 100644 index 0000000..c1cd897 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php @@ -0,0 +1,111 @@ +parser = new HTMLPurifier_URIParser(); + $this->embedsResource = (bool)$embeds_resource; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef_URI + */ + public function make($string) + { + $embeds = ($string === 'embedded'); + return new HTMLPurifier_AttrDef_URI($embeds); + } + + /** + * @param string $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($uri, $config, $context) + { + if ($config->get('URI.Disable')) { + return false; + } + + $uri = $this->parseCDATA($uri); + + // parse the URI + $uri = $this->parser->parse($uri); + if ($uri === false) { + return false; + } + + // add embedded flag to context for validators + $context->register('EmbeddedURI', $this->embedsResource); + + $ok = false; + do { + + // generic validation + $result = $uri->validate($config, $context); + if (!$result) { + break; + } + + // chained filtering + $uri_def = $config->getDefinition('URI'); + $result = $uri_def->filter($uri, $config, $context); + if (!$result) { + break; + } + + // scheme-specific validation + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + break; + } + if ($this->embedsResource && !$scheme_obj->browsable) { + break; + } + $result = $scheme_obj->validate($uri, $config, $context); + if (!$result) { + break; + } + + // Post chained filtering + $result = $uri_def->postFilter($uri, $config, $context); + if (!$result) { + break; + } + + // survived gauntlet + $ok = true; + + } while (false); + + $context->destroy('EmbeddedURI'); + if (!$ok) { + return false; + } + // back to string + return $uri->toString(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php new file mode 100644 index 0000000..daf32b7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php @@ -0,0 +1,20 @@ +" + // that needs more percent encoding to be done + if ($string == '') { + return false; + } + $string = trim($string); + $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string); + return $result ? $string : false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php new file mode 100644 index 0000000..17a97c1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php @@ -0,0 +1,136 @@ +ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); + $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $length = strlen($string); + // empty hostname is OK; it's usually semantically equivalent: + // the default host as defined by a URI scheme is used: + // + // If the URI scheme defines a default for host, then that + // default applies when the host subcomponent is undefined + // or when the registered name is empty (zero length). + if ($string === '') { + return ''; + } + if ($length > 1 && $string[0] === '[' && $string[$length - 1] === ']') { + //IPv6 + $ip = substr($string, 1, $length - 2); + $valid = $this->ipv6->validate($ip, $config, $context); + if ($valid === false) { + return false; + } + return '[' . $valid . ']'; + } + + // need to do checks on unusual encodings too + $ipv4 = $this->ipv4->validate($string, $config, $context); + if ($ipv4 !== false) { + return $ipv4; + } + + // A regular domain name. + + // This doesn't match I18N domain names, but we don't have proper IRI support, + // so force users to insert Punycode. + + // Underscores defined as Unreserved Characters in RFC 3986 are + // allowed in a URI. There are cases where we want to consider a + // URI containing "_" such as "_dmarc.example.com". + // Underscores are not allowed in the default. If you want to + // allow it, set Core.AllowHostnameUnderscore to true. + $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; + + // Based off of RFC 1738, but amended so that + // as per RFC 3696, the top label need only not be all numeric. + // The productions describing this are: + $a = '[a-z]'; // alpha + $an = "[a-z0-9$underscore]"; // alphanum + $and = "[a-z0-9-$underscore]"; // alphanum | "-" + // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + $domainlabel = "$an(?:$and*$an)?"; + // AMENDED as per RFC 3696 + // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // side condition: not all numeric + $toplabel = "$an(?:$and*$an)?"; + // hostname = *( domainlabel "." ) toplabel [ "." ] + if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) { + if (!ctype_digit($matches[1])) { + return $string; + } + } + + // PHP 5.3 and later support this functionality natively + if (function_exists('idn_to_ascii')) { + if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) { + $string = idn_to_ascii($string, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $string = idn_to_ascii($string); + } + + // If we have Net_IDNA2 support, we can support IRIs by + // punycoding them. (This is the most portable thing to do, + // since otherwise we have to assume browsers support + } elseif ($config->get('Core.EnableIDNA') && class_exists('Net_IDNA2')) { + $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); + // we need to encode each period separately + $parts = explode('.', $string); + try { + $new_parts = array(); + foreach ($parts as $part) { + $encodable = false; + for ($i = 0, $c = strlen($part); $i < $c; $i++) { + if (ord($part[$i]) > 0x7a) { + $encodable = true; + break; + } + } + if (!$encodable) { + $new_parts[] = $part; + } else { + $new_parts[] = $idna->encode($part); + } + } + $string = implode('.', $new_parts); + } catch (Exception $e) { + // XXX error reporting + } + } + // Try again + if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { + return $string; + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php new file mode 100644 index 0000000..30ac16c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php @@ -0,0 +1,45 @@ +ip4) { + $this->_loadRegex(); + } + + if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) { + return $aIP; + } + return false; + } + + /** + * Lazy load function to prevent regex from being stuffed in + * cache. + */ + protected function _loadRegex() + { + $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255 + $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php new file mode 100644 index 0000000..dc4ef62 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php @@ -0,0 +1,89 @@ +ip4) { + $this->_loadRegex(); + } + + $original = $aIP; + + $hex = '[0-9a-fA-F]'; + $blk = '(?:' . $hex . '{1,4})'; + $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128 + + // prefix check + if (strpos($aIP, '/') !== false) { + if (preg_match('#' . $pre . '$#s', $aIP, $find)) { + $aIP = substr($aIP, 0, 0 - strlen($find[0])); + unset($find); + } else { + return false; + } + } + + // IPv4-compatibility check + if (preg_match('#(?<=:' . ')' . $this->ip4 . '$#s', $aIP, $find)) { + $aIP = substr($aIP, 0, 0 - strlen($find[0])); + $ip = explode('.', $find[0]); + $ip = array_map('dechex', $ip); + $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3]; + unset($find, $ip); + } + + // compression check + $aIP = explode('::', $aIP); + $c = count($aIP); + if ($c > 2) { + return false; + } elseif ($c == 2) { + list($first, $second) = $aIP; + $first = explode(':', $first); + $second = explode(':', $second); + + if (count($first) + count($second) > 8) { + return false; + } + + while (count($first) < 8) { + array_push($first, '0'); + } + + array_splice($first, 8 - count($second), 8, $second); + $aIP = $first; + unset($first, $second); + } else { + $aIP = explode(':', $aIP[0]); + } + $c = count($aIP); + + if ($c != 8) { + return false; + } + + // All the pieces should be 16-bit hex strings. Are they? + foreach ($aIP as $piece) { + if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece))) { + return false; + } + } + return $original; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php new file mode 100644 index 0000000..b428331 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php @@ -0,0 +1,60 @@ +confiscateAttr($attr, 'background'); + // some validation should happen here + + $this->prependCSS($attr, "background-image:url($background);"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php new file mode 100644 index 0000000..d769c6f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php @@ -0,0 +1,27 @@ +get('Attr.DefaultTextDir'); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php new file mode 100644 index 0000000..0f51fd2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php @@ -0,0 +1,28 @@ +confiscateAttr($attr, 'bgcolor'); + // some validation should happen here + + $this->prependCSS($attr, "background-color:$bgcolor;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php new file mode 100644 index 0000000..f25cd01 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php @@ -0,0 +1,47 @@ +attr = $attr; + $this->css = $css; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + unset($attr[$this->attr]); + $this->prependCSS($attr, $this->css); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php new file mode 100644 index 0000000..057dc01 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php @@ -0,0 +1,26 @@ +confiscateAttr($attr, 'border'); + // some validation should happen here + $this->prependCSS($attr, "border:{$border_width}px solid;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php new file mode 100644 index 0000000..7ccd0e3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php @@ -0,0 +1,68 @@ +attr = $attr; + $this->enumToCSS = $enum_to_css; + $this->caseSensitive = (bool)$case_sensitive; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + + $value = trim($attr[$this->attr]); + unset($attr[$this->attr]); + + if (!$this->caseSensitive) { + $value = strtolower($value); + } + + if (!isset($this->enumToCSS[$value])) { + return $attr; + } + $this->prependCSS($attr, $this->enumToCSS[$value]); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php new file mode 100644 index 0000000..235ebb3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php @@ -0,0 +1,47 @@ +get('Core.RemoveInvalidImg')) { + return $attr; + } + $attr['src'] = $config->get('Attr.DefaultInvalidImage'); + $src = false; + } + + if (!isset($attr['alt'])) { + if ($src) { + $alt = $config->get('Attr.DefaultImageAlt'); + if ($alt === null) { + $attr['alt'] = basename($attr['src']); + } else { + $attr['alt'] = $alt; + } + } else { + $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php new file mode 100644 index 0000000..350b335 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php @@ -0,0 +1,61 @@ + array('left', 'right'), + 'vspace' => array('top', 'bottom') + ); + + /** + * @param string $attr + */ + public function __construct($attr) + { + $this->attr = $attr; + if (!isset($this->css[$attr])) { + trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); + } + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + + $width = $this->confiscateAttr($attr, $this->attr); + // some validation could happen here + + if (!isset($this->css[$this->attr])) { + return $attr; + } + + $style = ''; + foreach ($this->css[$this->attr] as $suffix) { + $property = "margin-$suffix"; + $style .= "$property:{$width}px;"; + } + $this->prependCSS($attr, $style); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php new file mode 100644 index 0000000..3ab47ed --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php @@ -0,0 +1,56 @@ +pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $t = 'text'; + } else { + $t = strtolower($attr['type']); + } + if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') { + unset($attr['checked']); + } + if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') { + unset($attr['maxlength']); + } + if (isset($attr['size']) && $t !== 'text' && $t !== 'password') { + $result = $this->pixels->validate($attr['size'], $config, $context); + if ($result === false) { + unset($attr['size']); + } else { + $attr['size'] = $result; + } + } + if (isset($attr['src']) && $t !== 'image') { + unset($attr['src']); + } + if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) { + $attr['value'] = ''; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php new file mode 100644 index 0000000..5b0aff0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php @@ -0,0 +1,31 @@ +name = $name; + $this->cssName = $css_name ? $css_name : $name; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->name])) { + return $attr; + } + $length = $this->confiscateAttr($attr, $this->name); + if (ctype_digit($length)) { + $length .= 'px'; + } + $this->prependCSS($attr, $this->cssName . ":$length;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php new file mode 100644 index 0000000..63cce68 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php @@ -0,0 +1,33 @@ +get('HTML.Attr.Name.UseCDATA')) { + return $attr; + } + if (!isset($attr['name'])) { + return $attr; + } + $id = $this->confiscateAttr($attr, 'name'); + if (isset($attr['id'])) { + return $attr; + } + $attr['id'] = $id; + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php new file mode 100644 index 0000000..5a1fdbb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php @@ -0,0 +1,46 @@ +idDef = new HTMLPurifier_AttrDef_HTML_ID(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['name'])) { + return $attr; + } + $name = $attr['name']; + if (isset($attr['id']) && $attr['id'] === $name) { + return $attr; + } + $result = $this->idDef->validate($name, $config, $context); + if ($result === false) { + unset($attr['name']); + } else { + $attr['name'] = $result; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php new file mode 100644 index 0000000..1057ebe --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php @@ -0,0 +1,52 @@ +parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isLocal($config, $context)) { + if (isset($attr['rel'])) { + $rels = explode(' ', $attr['rel']); + if (!in_array('nofollow', $rels)) { + $rels[] = 'nofollow'; + } + $attr['rel'] = implode(' ', $rels); + } else { + $attr['rel'] = 'nofollow'; + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php new file mode 100644 index 0000000..231c81a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php @@ -0,0 +1,25 @@ +uri = new HTMLPurifier_AttrDef_URI(true); // embedded + $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent')); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // If we add support for other objects, we'll need to alter the + // transforms. + switch ($attr['name']) { + // application/x-shockwave-flash + // Keep this synchronized with Injector/SafeObject.php + case 'allowScriptAccess': + $attr['value'] = 'never'; + break; + case 'allowNetworking': + $attr['value'] = 'internal'; + break; + case 'allowFullScreen': + if ($config->get('HTML.FlashAllowFullScreen')) { + $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false'; + } else { + $attr['value'] = 'false'; + } + break; + case 'wmode': + $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); + break; + case 'movie': + case 'src': + $attr['name'] = "movie"; + $attr['value'] = $this->uri->validate($attr['value'], $config, $context); + break; + case 'flashvars': + // we're going to allow arbitrary inputs to the SWF, on + // the reasoning that it could only hack the SWF, not us. + break; + // add other cases to support other param name/value pairs + default: + $attr['name'] = $attr['value'] = null; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php new file mode 100644 index 0000000..b7057bb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php @@ -0,0 +1,23 @@ + + */ +class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $attr['type'] = 'text/javascript'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php new file mode 100644 index 0000000..cc30ab8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php @@ -0,0 +1,49 @@ +parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + + // Ignore invalid schemes (e.g. `javascript:`) + if (!($scheme = $url->getSchemeObj($config, $context))) { + return $attr; + } + + if ($scheme->browsable && !$url->isBenign($config, $context)) { + $attr['target'] = '_blank'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php new file mode 100644 index 0000000..1db3c6c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php @@ -0,0 +1,37 @@ + + */ +class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // Calculated from Firefox + if (!isset($attr['cols'])) { + $attr['cols'] = '22'; + } + if (!isset($attr['rows'])) { + $attr['rows'] = '3'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php new file mode 100644 index 0000000..62575ca --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php @@ -0,0 +1,97 @@ +info['Enum'] = new HTMLPurifier_AttrDef_Enum(); + $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); + + $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); + $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); + $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); + $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); + $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); + $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); + $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); + $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); + $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); + $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); + $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); + $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); + $this->info['ContentEditable'] = new HTMLPurifier_AttrDef_HTML_ContentEditable(); + + // unimplemented aliases + $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); + + // "proprietary" types + $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); + + // number is really a positive integer (one or more digits) + // FIXME: ^^ not always, see start and value of list items + $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); + } + + private static function makeEnum($in) + { + return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); + } + + /** + * Retrieves a type + * @param string $type String type name + * @return HTMLPurifier_AttrDef Object AttrDef for type + */ + public function get($type) + { + // determine if there is any extra info tacked on + if (strpos($type, '#') !== false) { + list($type, $string) = explode('#', $type, 2); + } else { + $string = ''; + } + + if (!isset($this->info[$type])) { + throw new Exception('Cannot retrieve undefined attribute type ' . $type); + return; + } + return $this->info[$type]->make($string); + } + + /** + * Sets a new implementation for a type + * @param string $type String type name + * @param HTMLPurifier_AttrDef $impl Object AttrDef for type + */ + public function set($type, $impl) + { + $this->info[$type] = $impl; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php new file mode 100644 index 0000000..350330b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php @@ -0,0 +1,178 @@ +getHTMLDefinition(); + $e =& $context->get('ErrorCollector', true); + + // initialize IDAccumulator if necessary + $ok =& $context->get('IDAccumulator', true); + if (!$ok) { + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + } + + // initialize CurrentToken if necessary + $current_token =& $context->get('CurrentToken', true); + if (!$current_token) { + $context->register('CurrentToken', $token); + } + + if (!$token instanceof HTMLPurifier_Token_Start && + !$token instanceof HTMLPurifier_Token_Empty + ) { + return; + } + + // create alias to global definition array, see also $defs + // DEFINITION CALL + $d_defs = $definition->info_global_attr; + + // don't update token until the very end, to ensure an atomic update + $attr = $token->attr; + + // do global transformations (pre) + // nothing currently utilizes this + foreach ($definition->info_attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // do local transformations only applicable to this element (pre) + // ex.

to

+ foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // create alias to this element's attribute definition array, see + // also $d_defs (global attribute definition array) + // DEFINITION CALL + $defs = $definition->info[$token->name]->attr; + + $attr_key = false; + $context->register('CurrentAttr', $attr_key); + + // iterate through all the attribute keypairs + // Watch out for name collisions: $key has previously been used + foreach ($attr as $attr_key => $value) { + + // call the definition + if (isset($defs[$attr_key])) { + // there is a local definition defined + if ($defs[$attr_key] === false) { + // We've explicitly been told not to allow this element. + // This is usually when there's a global definition + // that must be overridden. + // Theoretically speaking, we could have a + // AttrDef_DenyAll, but this is faster! + $result = false; + } else { + // validate according to the element's definition + $result = $defs[$attr_key]->validate( + $value, + $config, + $context + ); + } + } elseif (isset($d_defs[$attr_key])) { + // there is a global definition defined, validate according + // to the global definition + $result = $d_defs[$attr_key]->validate( + $value, + $config, + $context + ); + } else { + // system never heard of the attribute? DELETE! + $result = false; + } + + // put the results into effect + if ($result === false || $result === null) { + // this is a generic error message that should replaced + // with more specific ones when possible + if ($e) { + $e->send(E_ERROR, 'AttrValidator: Attribute removed'); + } + + // remove the attribute + unset($attr[$attr_key]); + } elseif (is_string($result)) { + // generally, if a substitution is happening, there + // was some sort of implicit correction going on. We'll + // delegate it to the attribute classes to say exactly what. + + // simple substitution + $attr[$attr_key] = $result; + } else { + // nothing happens + } + + // we'd also want slightly more complicated substitution + // involving an array as the return value, + // although we're not sure how colliding attributes would + // resolve (certain ones would be completely overridden, + // others would prepend themselves). + } + + $context->destroy('CurrentAttr'); + + // post transforms + + // global (error reporting untested) + foreach ($definition->info_attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // local (error reporting untested) + foreach ($definition->info[$token->name]->attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + $token->attr = $attr; + + // destroy CurrentToken if we made it ourselves + if (!$current_token) { + $context->destroy('CurrentToken'); + } + + } + + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php new file mode 100644 index 0000000..8805ecc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php @@ -0,0 +1,91 @@ + +if (!defined('PHP_EOL')) { + switch (strtoupper(substr(PHP_OS, 0, 3))) { + case 'WIN': + define('PHP_EOL', "\r\n"); + break; + case 'DAR': + define('PHP_EOL', "\r"); + break; + default: + define('PHP_EOL', "\n"); + } +} + +/** + * Bootstrap class that contains meta-functionality for HTML Purifier such as + * the autoload function. + * + * @note + * This class may be used without any other files from HTML Purifier. + */ +class HTMLPurifier_Bootstrap +{ + + /** + * Autoload function for HTML Purifier + * @param string $class Class to load + * @return bool + */ + public static function autoload($class) + { + $file = HTMLPurifier_Bootstrap::getPath($class); + if (!$file) { + return false; + } + // Technically speaking, it should be ok and more efficient to + // just do 'require', but Antonio Parraga reports that with + // Zend extensions such as Zend debugger and APC, this invariant + // may be broken. Since we have efficient alternatives, pay + // the cost here and avoid the bug. + require_once HTMLPURIFIER_PREFIX . '/' . $file; + return true; + } + + /** + * Returns the path for a specific class. + * @param string $class Class path to get + * @return string + */ + public static function getPath($class) + { + if (strncmp('HTMLPurifier', $class, 12) !== 0) { + return false; + } + // Custom implementations + if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { + $code = str_replace('_', '-', substr($class, 22)); + $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; + } else { + $file = str_replace('_', '/', $class) . '.php'; + } + if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) { + return false; + } + return $file; + } + + /** + * "Pre-registers" our autoloader on the SPL stack. + */ + public static function registerAutoload() + { + $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); + if (spl_autoload_functions() === false) { + spl_autoload_register($autoload); + } else { + // prepend flag exists, no need for shenanigans + spl_autoload_register($autoload, true, true); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php new file mode 100644 index 0000000..923d6f3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php @@ -0,0 +1,570 @@ +info['text-align'] = new HTMLPurifier_AttrDef_Enum( + ['left', 'right', 'center', 'justify'], + false + ); + + $this->info['direction'] = new HTMLPurifier_AttrDef_Enum( + ['ltr', 'rtl'], + false + ); + + $border_style = + $this->info['border-bottom-style'] = + $this->info['border-right-style'] = + $this->info['border-left-style'] = + $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( + [ + 'none', + 'hidden', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset' + ], + false + ); + + $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); + + $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( + ['none', 'left', 'right', 'both'], + false + ); + $this->info['float'] = new HTMLPurifier_AttrDef_Enum( + ['none', 'left', 'right'], + false + ); + $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( + ['normal', 'italic', 'oblique'], + false + ); + $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( + ['normal', 'small-caps'], + false + ); + + $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum(['none']), + new HTMLPurifier_AttrDef_CSS_URI() + ] + ); + + $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( + ['inside', 'outside'], + false + ); + $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( + [ + 'disc', + 'circle', + 'square', + 'decimal', + 'lower-roman', + 'upper-roman', + 'lower-alpha', + 'upper-alpha', + 'none' + ], + false + ); + $this->info['list-style-image'] = $uri_or_none; + + $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); + + $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( + ['capitalize', 'uppercase', 'lowercase', 'none'], + false + ); + $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + $this->info['background-image'] = $uri_or_none; + $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( + ['repeat', 'repeat-x', 'repeat-y', 'no-repeat'] + ); + $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( + ['scroll', 'fixed'] + ); + $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); + + $this->info['background-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum( + [ + 'auto', + 'cover', + 'contain', + ] + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ] + ); + + $border_color = + $this->info['border-top-color'] = + $this->info['border-bottom-color'] = + $this->info['border-left-color'] = + $this->info['border-right-color'] = + $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum(['transparent']), + new HTMLPurifier_AttrDef_CSS_Color() + ] + ); + + $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); + + $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); + + $border_width = + $this->info['border-top-width'] = + $this->info['border-bottom-width'] = + $this->info['border-left-width'] = + $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum(['thin', 'medium', 'thick']), + new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative + ] + ); + + $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); + + $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum(['normal']), + new HTMLPurifier_AttrDef_CSS_Length() + ] + ); + + $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum(['normal']), + new HTMLPurifier_AttrDef_CSS_Length() + ] + ); + + $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum( + [ + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large', + 'larger', + 'smaller' + ] + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ] + ); + + $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum(['normal']), + new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ] + ); + + $margin = + $this->info['margin-top'] = + $this->info['margin-bottom'] = + $this->info['margin-left'] = + $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(['auto']) + ] + ); + + $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); + + // non-negative + $padding = + $this->info['padding-top'] = + $this->info['padding-bottom'] = + $this->info['padding-left'] = + $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ] + ); + + $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); + + $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ] + ); + + $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(['auto']) + ] + ); + $trusted_min_wh = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + ] + ); + $trusted_max_wh = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(['none']) + ] + ); + $max = $config->get('CSS.MaxImgLength'); + + $this->info['width'] = + $this->info['height'] = + $max === null ? + $trusted_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(['auto']) + ] + ), + // For everyone else: + $trusted_wh + ); + $this->info['min-width'] = + $this->info['min-height'] = + $max === null ? + $trusted_min_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + // For everyone else: + $trusted_min_wh + ); + $this->info['max-width'] = + $this->info['max-height'] = + $max === null ? + $trusted_max_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(['none']) + ] + ), + // For everyone else: + $trusted_max_wh + ); + + $this->info['aspect-ratio'] = new HTMLPurifier_AttrDef_CSS_Multiple( + new HTMLPurifier_AttrDef_CSS_Composite([ + new HTMLPurifier_AttrDef_CSS_Ratio(), + new HTMLPurifier_AttrDef_Enum(['auto']), + ]) + ); + + // text-decoration and related shorthands + $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); + + $this->info['text-decoration-line'] = new HTMLPurifier_AttrDef_Enum( + ['none', 'underline', 'overline', 'line-through'] + ); + + $this->info['text-decoration-style'] = new HTMLPurifier_AttrDef_Enum( + ['solid', 'double', 'dotted', 'dashed', 'wavy'] + ); + + $this->info['text-decoration-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + $this->info['text-decoration-thickness'] = new HTMLPurifier_AttrDef_CSS_Composite([ + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(['auto', 'from-font']) + ]); + + $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); + + // this could use specialized code + $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( + [ + 'normal', + 'bold', + 'bolder', + 'lighter', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900' + ], + false + ); + + // MUST be called after other font properties, as it references + // a CSSDefinition object + $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); + + // same here + $this->info['border'] = + $this->info['border-bottom'] = + $this->info['border-top'] = + $this->info['border-left'] = + $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); + + $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum( + ['collapse', 'separate'] + ); + + $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum( + ['top', 'bottom'] + ); + + $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum( + ['auto', 'fixed'] + ); + + $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Enum( + [ + 'baseline', + 'sub', + 'super', + 'top', + 'text-top', + 'middle', + 'bottom', + 'text-bottom' + ] + ), + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ] + ); + + $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); + + // These CSS properties don't work on many browsers, but we live + // in THE FUTURE! + $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( + ['nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line'] + ); + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + if ($config->get('CSS.Trusted')) { + $this->doSetupTrusted($config); + } + + $allow_important = $config->get('CSS.AllowImportant'); + // wrap all attr-defs with decorator that handles !important + foreach ($this->info as $k => $v) { + $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); + } + + $this->setupConfigStuff($config); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupProprietary($config) + { + // Internet Explorer only scrollbar colors + $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + // vendor specific prefixes of opacity + $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + + // only opacity, for now + $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); + + // more CSS3 + $this->info['page-break-after'] = + $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( + [ + 'auto', + 'always', + 'avoid', + 'left', + 'right' + ] + ); + $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(['auto', 'avoid']); + + $border_radius = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative + new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative + ]); + + $this->info['border-top-left-radius'] = + $this->info['border-top-right-radius'] = + $this->info['border-bottom-right-radius'] = + $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2); + // TODO: support SLASH syntax + $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4); + + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTricky($config) + { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum( + [ + 'inline', + 'block', + 'list-item', + 'run-in', + 'compact', + 'marker', + 'table', + 'inline-block', + 'inline-table', + 'table-row-group', + 'table-header-group', + 'table-footer-group', + 'table-row', + 'table-column-group', + 'table-column', + 'table-cell', + 'table-caption', + 'none' + ] + ); + $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum( + ['visible', 'hidden', 'collapse'] + ); + $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(['visible', 'hidden', 'auto', 'scroll']); + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTrusted($config) + { + $this->info['position'] = new HTMLPurifier_AttrDef_Enum( + ['static', 'relative', 'absolute', 'fixed'] + ); + $this->info['top'] = + $this->info['left'] = + $this->info['right'] = + $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(['auto']), + ] + ); + $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( + [ + new HTMLPurifier_AttrDef_Integer(), + new HTMLPurifier_AttrDef_Enum(['auto']), + ] + ); + } + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_Config $config + * @todo Refactor duplicate elements into common class (probably using + * composition, not inheritance). + */ + protected function setupConfigStuff($config) + { + // setup allowed elements + $support = "(for information on implementing this, see the " . + "support forums) "; + $allowed_properties = $config->get('CSS.AllowedProperties'); + if ($allowed_properties !== null) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_properties[$name])) { + unset($this->info[$name]); + } + unset($allowed_properties[$name]); + } + // emit errors + foreach ($allowed_properties as $name => $d) { + // :TODO: Is this htmlspecialchars() call really necessary? + $name = htmlspecialchars($name); + trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); + } + } + + $forbidden_properties = $config->get('CSS.ForbiddenProperties'); + if ($forbidden_properties !== null) { + foreach ($this->info as $name => $d) { + if (isset($forbidden_properties[$name])) { + unset($this->info[$name]); + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php new file mode 100644 index 0000000..8eb17b8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php @@ -0,0 +1,52 @@ +elements; + } + + /** + * Validates nodes according to definition and returns modification. + * + * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @param HTMLPurifier_Context $context HTMLPurifier_Context object + * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children + */ + abstract public function validateChildren($children, $config, $context); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php new file mode 100644 index 0000000..7439be2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php @@ -0,0 +1,67 @@ +inline = new HTMLPurifier_ChildDef_Optional($inline); + $this->block = new HTMLPurifier_ChildDef_Optional($block); + $this->elements = $this->block->elements; + } + + /** + * @param HTMLPurifier_Node[] $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function validateChildren($children, $config, $context) + { + if ($context->get('IsInline') === false) { + return $this->block->validateChildren( + $children, + $config, + $context + ); + } else { + return $this->inline->validateChildren( + $children, + $config, + $context + ); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php new file mode 100644 index 0000000..f515888 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php @@ -0,0 +1,102 @@ +dtd_regex = $dtd_regex; + $this->_compileRegex(); + } + + /** + * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex) + */ + protected function _compileRegex() + { + $raw = str_replace(' ', '', $this->dtd_regex); + if ($raw[0] != '(') { + $raw = "($raw)"; + } + $el = '[#a-zA-Z0-9_.-]+'; + $reg = $raw; + + // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M + // DOING! Seriously: if there's problems, please report them. + + // collect all elements into the $elements array + preg_match_all("/$el/", $reg, $matches); + foreach ($matches[0] as $match) { + $this->elements[$match] = true; + } + + // setup all elements as parentheticals with leading commas + $reg = preg_replace("/$el/", '(,\\0)', $reg); + + // remove commas when they were not solicited + $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); + + // remove all non-paranthetical commas: they are handled by first regex + $reg = preg_replace("/,\(/", '(', $reg); + + $this->_pcre_regex = $reg; + } + + /** + * @param HTMLPurifier_Node[] $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function validateChildren($children, $config, $context) + { + $list_of_children = ''; + $nesting = 0; // depth into the nest + foreach ($children as $node) { + if (!empty($node->is_whitespace)) { + continue; + } + $list_of_children .= $node->name . ','; + } + // add leading comma to deal with stray comma declarations + $list_of_children = ',' . rtrim($list_of_children, ','); + $okay = + preg_match( + '/^,?' . $this->_pcre_regex . '$/', + $list_of_children + ); + return (bool)$okay; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php new file mode 100644 index 0000000..a8a6cbd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php @@ -0,0 +1,38 @@ + true, 'ul' => true, 'ol' => true); + + public $whitespace; + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + // Flag for subclasses + $this->whitespace = false; + + // if there are no tokens, delete parent node + if (empty($children)) { + return false; + } + + // if li is not allowed, delete parent node + if (!isset($config->getHTMLDefinition()->info['li'])) { + trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING); + return false; + } + + // the new set of children + $result = array(); + + // a little sanity check to make sure it's not ALL whitespace + $all_whitespace = true; + + $current_li = null; + + foreach ($children as $node) { + if (!empty($node->is_whitespace)) { + $result[] = $node; + continue; + } + $all_whitespace = false; // phew, we're not talking about whitespace + + if ($node->name === 'li') { + // good + $current_li = $node; + $result[] = $node; + } else { + // we want to tuck this into the previous li + // Invariant: we expect the node to be ol/ul + // ToDo: Make this more robust in the case of not ol/ul + // by distinguishing between existing li and li created + // to handle non-list elements; non-list elements should + // not be appended to an existing li; only li created + // for non-list. This distinction is not currently made. + if ($current_li === null) { + $current_li = new HTMLPurifier_Node_Element('li'); + $result[] = $current_li; + } + $current_li->children[] = $node; + $current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo + } + } + if (empty($result)) { + return false; + } + if ($all_whitespace) { + return false; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php new file mode 100644 index 0000000..b946806 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php @@ -0,0 +1,45 @@ +whitespace) { + return $children; + } else { + return array(); + } + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php new file mode 100644 index 0000000..0d1c8f5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php @@ -0,0 +1,118 @@ + $x) { + $elements[$i] = true; + if (empty($i)) { + unset($elements[$i]); + } // remove blank + } + } + $this->elements = $elements; + } + + /** + * @type bool + */ + public $allow_empty = false; + + /** + * @type string + */ + public $type = 'required'; + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + // Flag for subclasses + $this->whitespace = false; + + // if there are no tokens, delete parent node + if (empty($children)) { + return false; + } + + // the new set of children + $result = array(); + + // whether or not parsed character data is allowed + // this controls whether or not we silently drop a tag + // or generate escaped HTML from it + $pcdata_allowed = isset($this->elements['#PCDATA']); + + // a little sanity check to make sure it's not ALL whitespace + $all_whitespace = true; + + $stack = array_reverse($children); + while (!empty($stack)) { + $node = array_pop($stack); + if (!empty($node->is_whitespace)) { + $result[] = $node; + continue; + } + $all_whitespace = false; // phew, we're not talking about whitespace + + if (!isset($this->elements[$node->name])) { + // special case text + // XXX One of these ought to be redundant or something + if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) { + $result[] = $node; + continue; + } + // spill the child contents in + // ToDo: Make configurable + if ($node instanceof HTMLPurifier_Node_Element) { + for ($i = count($node->children) - 1; $i >= 0; $i--) { + $stack[] = $node->children[$i]; + } + continue; + } + continue; + } + $result[] = $node; + } + if (empty($result)) { + return false; + } + if ($all_whitespace) { + $this->whitespace = true; + return false; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php new file mode 100644 index 0000000..3270a46 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php @@ -0,0 +1,110 @@ +init($config); + return $this->fake_elements; + } + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + $this->init($config); + + // trick the parent class into thinking it allows more + $this->elements = $this->fake_elements; + $result = parent::validateChildren($children, $config, $context); + $this->elements = $this->real_elements; + + if ($result === false) { + return array(); + } + if ($result === true) { + $result = $children; + } + + $def = $config->getHTMLDefinition(); + $block_wrap_name = $def->info_block_wrapper; + $block_wrap = false; + $ret = array(); + + foreach ($result as $node) { + if ($block_wrap === false) { + if (($node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) || + ($node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) { + $block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper); + $ret[] = $block_wrap; + } + } else { + if ($node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) { + $block_wrap = false; + + } + } + if ($block_wrap) { + $block_wrap->children[] = $node; + } else { + $ret[] = $node; + } + } + return $ret; + } + + /** + * @param HTMLPurifier_Config $config + */ + private function init($config) + { + if (!$this->init) { + $def = $config->getHTMLDefinition(); + // allow all inline elements + $this->real_elements = $this->elements; + $this->fake_elements = $def->info_content_sets['Flow']; + $this->fake_elements['#PCDATA'] = true; + $this->init = true; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php new file mode 100644 index 0000000..d92205b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php @@ -0,0 +1,227 @@ + true, + 'tbody' => true, + 'thead' => true, + 'tfoot' => true, + 'caption' => true, + 'colgroup' => true, + 'col' => true + ); + + public function __construct() + { + } + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + if (empty($children)) { + return false; + } + + // only one of these elements is allowed in a table + $caption = false; + $thead = false; + $tfoot = false; + + // whitespace + $initial_ws = array(); + $after_caption_ws = array(); + $after_thead_ws = array(); + $after_tfoot_ws = array(); + + // as many of these as you want + $cols = array(); + $content = array(); + + $tbody_mode = false; // if true, then we need to wrap any stray + // s with a . + + $ws_accum =& $initial_ws; + + foreach ($children as $node) { + if ($node instanceof HTMLPurifier_Node_Comment) { + $ws_accum[] = $node; + continue; + } + switch ($node->name) { + case 'tbody': + $tbody_mode = true; + // fall through + case 'tr': + $content[] = $node; + $ws_accum =& $content; + break; + case 'caption': + // there can only be one caption! + if ($caption !== false) break; + $caption = $node; + $ws_accum =& $after_caption_ws; + break; + case 'thead': + $tbody_mode = true; + // XXX This breaks rendering properties with + // Firefox, which never floats a to + // the top. Ever. (Our scheme will float the + // first to the top.) So maybe + // s that are not first should be + // turned into ? Very tricky, indeed. + if ($thead === false) { + $thead = $node; + $ws_accum =& $after_thead_ws; + } else { + // Oops, there's a second one! What + // should we do? Current behavior is to + // transmutate the first and last entries into + // tbody tags, and then put into content. + // Maybe a better idea is to *attach + // it* to the existing thead or tfoot? + // We don't do this, because Firefox + // doesn't float an extra tfoot to the + // bottom like it does for the first one. + $node->name = 'tbody'; + $content[] = $node; + $ws_accum =& $content; + } + break; + case 'tfoot': + // see above for some aveats + $tbody_mode = true; + if ($tfoot === false) { + $tfoot = $node; + $ws_accum =& $after_tfoot_ws; + } else { + $node->name = 'tbody'; + $content[] = $node; + $ws_accum =& $content; + } + break; + case 'colgroup': + case 'col': + $cols[] = $node; + $ws_accum =& $cols; + break; + case '#PCDATA': + // How is whitespace handled? We treat is as sticky to + // the *end* of the previous element. So all of the + // nonsense we have worked on is to keep things + // together. + if (!empty($node->is_whitespace)) { + $ws_accum[] = $node; + } + break; + } + } + + if (empty($content) && $thead === false && $tfoot === false) { + return false; + } + + $ret = $initial_ws; + if ($caption !== false) { + $ret[] = $caption; + $ret = array_merge($ret, $after_caption_ws); + } + if ($cols !== false) { + $ret = array_merge($ret, $cols); + } + if ($thead !== false) { + $ret[] = $thead; + $ret = array_merge($ret, $after_thead_ws); + } + if ($tfoot !== false) { + $ret[] = $tfoot; + $ret = array_merge($ret, $after_tfoot_ws); + } + + if ($tbody_mode) { + // we have to shuffle tr into tbody + $current_tr_tbody = null; + + foreach($content as $node) { + if (!isset($node->name)) { + continue; + } + switch ($node->name) { + case 'tbody': + $current_tr_tbody = null; + $ret[] = $node; + break; + case 'tr': + if ($current_tr_tbody === null) { + $current_tr_tbody = new HTMLPurifier_Node_Element('tbody'); + $ret[] = $current_tr_tbody; + } + $current_tr_tbody->children[] = $node; + break; + case '#PCDATA': + //assert($node->is_whitespace); + if ($current_tr_tbody === null) { + $ret[] = $node; + } else { + $current_tr_tbody->children[] = $node; + } + break; + } + } + } else { + $ret = array_merge($ret, $content); + } + + return $ret; + + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php new file mode 100644 index 0000000..256408e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php @@ -0,0 +1,924 @@ +defaultPlist; + $this->plist = new HTMLPurifier_PropertyList($parent); + $this->def = $definition; // keep a copy around for checking + $this->parser = new HTMLPurifier_VarParser_Flexible(); + } + + /** + * Convenience constructor that creates a config object based on a mixed var + * @param mixed $config Variable that defines the state of the config + * object. Can be: a HTMLPurifier_Config() object, + * an array of directives based on loadArray(), + * or a string filename of an ini file. + * @param HTMLPurifier_ConfigSchema $schema Schema object + * @return HTMLPurifier_Config Configured object + */ + public static function create($config, $schema = null) + { + if ($config instanceof HTMLPurifier_Config) { + // pass-through + return $config; + } + if (!$schema) { + $ret = HTMLPurifier_Config::createDefault(); + } else { + $ret = new HTMLPurifier_Config($schema); + } + if (is_string($config)) { + $ret->loadIni($config); + } elseif (is_array($config)) $ret->loadArray($config); + return $ret; + } + + /** + * Creates a new config object that inherits from a previous one. + * @param HTMLPurifier_Config $config Configuration object to inherit from. + * @return HTMLPurifier_Config object with $config as its parent. + */ + public static function inherit(HTMLPurifier_Config $config) + { + return new HTMLPurifier_Config($config->def, $config->plist); + } + + /** + * Convenience constructor that creates a default configuration object. + * @return HTMLPurifier_Config default object. + */ + public static function createDefault() + { + $definition = HTMLPurifier_ConfigSchema::instance(); + $config = new HTMLPurifier_Config($definition); + return $config; + } + + /** + * Retrieves a value from the configuration. + * + * @param string $key String key + * @param mixed $a + * + * @return mixed + */ + public function get($key, $a = null) + { + if ($a !== null) { + $this->triggerError( + "Using deprecated API: use \$config->get('$key.$a') instead", + E_USER_WARNING + ); + $key = "$key.$a"; + } + if (!$this->finalized) { + $this->autoFinalize(); + } + if (!isset($this->def->info[$key])) { + // can't add % due to SimpleTest bug + $this->triggerError( + 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key), + E_USER_WARNING + ); + return; + } + if (isset($this->def->info[$key]->isAlias)) { + $d = $this->def->info[$key]; + $this->triggerError( + 'Cannot get value from aliased directive, use real name ' . $d->key, + E_USER_ERROR + ); + return; + } + if ($this->lock) { + list($ns) = explode('.', $key); + if ($ns !== $this->lock) { + $this->triggerError( + 'Cannot get value of namespace ' . $ns . ' when lock for ' . + $this->lock . + ' is active, this probably indicates a Definition setup method ' . + 'is accessing directives that are not within its namespace', + E_USER_ERROR + ); + return; + } + } + return $this->plist->get($key); + } + + /** + * Retrieves an array of directives to values from a given namespace + * + * @param string $namespace String namespace + * + * @return array + */ + public function getBatch($namespace) + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $full = $this->getAll(); + if (!isset($full[$namespace])) { + $this->triggerError( + 'Cannot retrieve undefined namespace ' . + htmlspecialchars($namespace), + E_USER_WARNING + ); + return; + } + return $full[$namespace]; + } + + /** + * Returns a SHA-1 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * + * @param string $namespace Namespace to get serial for + * + * @return string + * @note Revision is handled specially and is removed from the batch + * before processing! + */ + public function getBatchSerial($namespace) + { + if (empty($this->serials[$namespace])) { + $batch = $this->getBatch($namespace); + unset($batch['DefinitionRev']); + $this->serials[$namespace] = sha1(serialize($batch)); + } + return $this->serials[$namespace]; + } + + /** + * Returns a SHA-1 signature for the entire configuration object + * that uniquely identifies that particular configuration + * + * @return string + */ + public function getSerial() + { + if (empty($this->serial)) { + $this->serial = sha1(serialize($this->getAll())); + } + return $this->serial; + } + + /** + * Retrieves all directives, organized by namespace + * + * @warning This is a pretty inefficient function, avoid if you can + */ + public function getAll() + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $ret = array(); + foreach ($this->plist->squash() as $name => $value) { + list($ns, $key) = explode('.', $name, 2); + $ret[$ns][$key] = $value; + } + return $ret; + } + + /** + * Sets a value to configuration. + * + * @param string $key key + * @param mixed $value value + * @param mixed $a + */ + public function set($key, $value, $a = null) + { + if (strpos($key, '.') === false) { + $namespace = $key; + $directive = $value; + $value = $a; + $key = "$key.$directive"; + $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); + } else { + list($namespace) = explode('.', $key); + } + if ($this->isFinalized('Cannot set directive after finalization')) { + return; + } + if (!isset($this->def->info[$key])) { + $this->triggerError( + 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', + E_USER_WARNING + ); + return; + } + $def = $this->def->info[$key]; + + if (isset($def->isAlias)) { + if ($this->aliasMode) { + $this->triggerError( + 'Double-aliases not allowed, please fix '. + 'ConfigSchema bug with' . $key, + E_USER_ERROR + ); + return; + } + $this->aliasMode = true; + $this->set($def->key, $value); + $this->aliasMode = false; + $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); + return; + } + + // Raw type might be negative when using the fully optimized form + // of stdClass, which indicates allow_null == true + $rtype = is_int($def) ? $def : $def->type; + if ($rtype < 0) { + $type = -$rtype; + $allow_null = true; + } else { + $type = $rtype; + $allow_null = isset($def->allow_null); + } + + try { + $value = $this->parser->parse($value, $type, $allow_null); + } catch (HTMLPurifier_VarParserException $e) { + $this->triggerError( + 'Value for ' . $key . ' is of invalid type, should be ' . + HTMLPurifier_VarParser::getTypeName($type), + E_USER_WARNING + ); + return; + } + if (is_string($value) && is_object($def)) { + // resolve value alias if defined + if (isset($def->aliases[$value])) { + $value = $def->aliases[$value]; + } + // check to see if the value is allowed + if (isset($def->allowed) && !isset($def->allowed[$value])) { + $this->triggerError( + 'Value not supported, valid values are: ' . + $this->_listify($def->allowed), + E_USER_WARNING + ); + return; + } + } + $this->plist->set($key, $value); + + // reset definitions if the directives they depend on changed + // this is a very costly process, so it's discouraged + // with finalization + if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { + $this->definitions[$namespace] = null; + } + + $this->serials[$namespace] = false; + } + + /** + * Convenience function for error reporting + * + * @param array $lookup + * + * @return string + */ + private function _listify($lookup) + { + $list = array(); + foreach ($lookup as $name => $b) { + $list[] = $name; + } + return implode(', ', $list); + } + + /** + * Retrieves object reference to the HTML definition. + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawHTMLDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_HTMLDefinition|null + */ + public function getHTMLDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('HTML', $raw, $optimized); + } + + /** + * Retrieves object reference to the CSS definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawCSSDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_CSSDefinition|null + */ + public function getCSSDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('CSS', $raw, $optimized); + } + + /** + * Retrieves object reference to the URI definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawURIDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_URIDefinition|null + */ + public function getURIDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('URI', $raw, $optimized); + } + + /** + * Retrieves a definition + * + * @param string $type Type of definition: HTML, CSS, etc + * @param bool $raw Whether or not definition should be returned raw + * @param bool $optimized Only has an effect when $raw is true. Whether + * or not to return null if the result is already present in + * the cache. This is off by default for backwards + * compatibility reasons, but you need to do things this + * way in order to ensure that caching is done properly. + * Check out enduser-customize.html for more details. + * We probably won't ever change this default, as much as the + * maybe semantics is the "right thing to do." + * + * @throws HTMLPurifier_Exception + * @return HTMLPurifier_Definition|null + */ + public function getDefinition($type, $raw = false, $optimized = false) + { + if ($optimized && !$raw) { + throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); + } + if (!$this->finalized) { + $this->autoFinalize(); + } + // temporarily suspend locks, so we can handle recursive definition calls + $lock = $this->lock; + $this->lock = null; + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); + $this->lock = $lock; + if (!$raw) { + // full definition + // --------------- + // check if definition is in memory + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + // check if the definition is setup + if ($def->setup) { + return $def; + } else { + $def->setup($this); + if ($def->optimized) { + $cache->add($def, $this); + } + return $def; + } + } + // check if definition is in cache + $def = $cache->get($this); + if ($def) { + // definition in cache, save to memory and return it + $this->definitions[$type] = $def; + return $def; + } + // initialize it + $def = $this->initDefinition($type); + // set it up + $this->lock = $type; + $def->setup($this); + $this->lock = null; + // save in cache + $cache->add($def, $this); + // return it + return $def; + } else { + // raw definition + // -------------- + // check preconditions + $def = null; + if ($optimized) { + if (is_null($this->get($type . '.DefinitionID'))) { + // fatally error out if definition ID not set + throw new HTMLPurifier_Exception( + "Cannot retrieve raw version without specifying %$type.DefinitionID" + ); + } + } + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + if ($def->setup && !$optimized) { + $extra = $this->chatty ? + " (try moving this code block earlier in your initialization)" : + ""; + throw new HTMLPurifier_Exception( + "Cannot retrieve raw definition after it has already been setup" . + $extra + ); + } + if ($def->optimized === null) { + $extra = $this->chatty ? " (try flushing your cache)" : ""; + throw new HTMLPurifier_Exception( + "Optimization status of definition is unknown" . $extra + ); + } + if ($def->optimized !== $optimized) { + $msg = $optimized ? "optimized" : "unoptimized"; + $extra = $this->chatty ? + " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" + : ""; + throw new HTMLPurifier_Exception( + "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra + ); + } + } + // check if definition was in memory + if ($def) { + if ($def->setup) { + // invariant: $optimized === true (checked above) + return null; + } else { + return $def; + } + } + // if optimized, check if definition was in cache + // (because we do the memory check first, this formulation + // is prone to cache slamming, but I think + // guaranteeing that either /all/ of the raw + // setup code or /none/ of it is run is more important.) + if ($optimized) { + // This code path only gets run once; once we put + // something in $definitions (which is guaranteed by the + // trailing code), we always short-circuit above. + $def = $cache->get($this); + if ($def) { + // save the full definition for later, but don't + // return it yet + $this->definitions[$type] = $def; + return null; + } + } + // check invariants for creation + if (!$optimized) { + if (!is_null($this->get($type . '.DefinitionID'))) { + if ($this->chatty) { + $this->triggerError( + 'Due to a documentation error in previous version of HTML Purifier, your ' . + 'definitions are not being cached. If this is OK, you can remove the ' . + '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . + 'modify your code to use maybeGetRawDefinition, and test if the returned ' . + 'value is null before making any edits (if it is null, that means that a ' . + 'cached version is available, and no raw operations are necessary). See ' . + '' . + 'Customize for more details', + E_USER_WARNING + ); + } else { + $this->triggerError( + "Useless DefinitionID declaration", + E_USER_WARNING + ); + } + } + } + // initialize it + $def = $this->initDefinition($type); + $def->optimized = $optimized; + return $def; + } + throw new HTMLPurifier_Exception("The impossible happened!"); + } + + /** + * Initialise definition + * + * @param string $type What type of definition to create + * + * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition + * @throws HTMLPurifier_Exception + */ + private function initDefinition($type) + { + // quick checks failed, let's create the object + if ($type == 'HTML') { + $def = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $def = new HTMLPurifier_CSSDefinition(); + } elseif ($type == 'URI') { + $def = new HTMLPurifier_URIDefinition(); + } else { + throw new HTMLPurifier_Exception( + "Definition of $type type not supported" + ); + } + $this->definitions[$type] = $def; + return $def; + } + + public function maybeGetRawDefinition($name) + { + return $this->getDefinition($name, true, true); + } + + /** + * @return HTMLPurifier_HTMLDefinition|null + */ + public function maybeGetRawHTMLDefinition() + { + return $this->getDefinition('HTML', true, true); + } + + /** + * @return HTMLPurifier_CSSDefinition|null + */ + public function maybeGetRawCSSDefinition() + { + return $this->getDefinition('CSS', true, true); + } + + /** + * @return HTMLPurifier_URIDefinition|null + */ + public function maybeGetRawURIDefinition() + { + return $this->getDefinition('URI', true, true); + } + + /** + * Loads configuration values from an array with the following structure: + * Namespace.Directive => Value + * + * @param array $config_array Configuration associative array + */ + public function loadArray($config_array) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + foreach ($config_array as $key => $value) { + $key = str_replace('_', '.', $key); + if (strpos($key, '.') !== false) { + $this->set($key, $value); + } else { + $namespace = $key; + $namespace_values = $value; + foreach ($namespace_values as $directive => $value2) { + $this->set($namespace .'.'. $directive, $value2); + } + } + } + } + + /** + * Returns a list of array(namespace, directive) for all directives + * that are allowed in a web-form context as per an allowed + * namespaces/directives list. + * + * @param array $allowed List of allowed namespaces/directives + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function getAllowedDirectivesForForm($allowed, $schema = null) + { + if (!$schema) { + $schema = HTMLPurifier_ConfigSchema::instance(); + } + if ($allowed !== true) { + if (is_string($allowed)) { + $allowed = array($allowed); + } + $allowed_ns = array(); + $allowed_directives = array(); + $blacklisted_directives = array(); + foreach ($allowed as $ns_or_directive) { + if (strpos($ns_or_directive, '.') !== false) { + // directive + if ($ns_or_directive[0] == '-') { + $blacklisted_directives[substr($ns_or_directive, 1)] = true; + } else { + $allowed_directives[$ns_or_directive] = true; + } + } else { + // namespace + $allowed_ns[$ns_or_directive] = true; + } + } + } + $ret = array(); + foreach ($schema->info as $key => $def) { + list($ns, $directive) = explode('.', $key, 2); + if ($allowed !== true) { + if (isset($blacklisted_directives["$ns.$directive"])) { + continue; + } + if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) { + continue; + } + } + if (isset($def->isAlias)) { + continue; + } + if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') { + continue; + } + $ret[] = array($ns, $directive); + } + return $ret; + } + + /** + * Loads configuration values from $_GET/$_POST that were posted + * via ConfigForm + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return mixed + */ + public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); + $config = HTMLPurifier_Config::create($ret, $schema); + return $config; + } + + /** + * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + */ + public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); + $this->loadArray($ret); + } + + /** + * Prepares an array from a form into something usable for the more + * strict parts of HTMLPurifier_Config + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + if ($index !== false) { + $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + } + $mq = $mq_fix && version_compare(PHP_VERSION, '7.4.0', '<') && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); + $ret = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $skey = "$ns.$directive"; + if (!empty($array["Null_$skey"])) { + $ret[$ns][$directive] = null; + continue; + } + if (!isset($array[$skey])) { + continue; + } + $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; + $ret[$ns][$directive] = $value; + } + return $ret; + } + + /** + * Loads configuration values from an ini file + * + * @param string $filename Name of ini file + */ + public function loadIni($filename) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + $array = parse_ini_file($filename, true); + $this->loadArray($array); + } + + /** + * Checks whether or not the configuration object is finalized. + * + * @param string|bool $error String error message, or false for no error + * + * @return bool + */ + public function isFinalized($error = false) + { + if ($this->finalized && $error) { + $this->triggerError($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + public function autoFinalize() + { + if ($this->autoFinalize) { + $this->finalize(); + } else { + $this->plist->squash(true); + } + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + public function finalize() + { + $this->finalized = true; + $this->parser = null; + } + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information OUTSIDE of HTMLPurifier_Config. + * + * @param string $msg An error message + * @param int $no An error number + */ + protected function triggerError($msg, $no) + { + // determine previous stack frame + $extra = ''; + if ($this->chatty) { + $trace = debug_backtrace(); + // zip(tail(trace), trace) -- but PHP is not Haskell har har + for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { + // XXX this is not correct on some versions of HTML Purifier + if (isset($trace[$i + 1]['class']) && $trace[$i + 1]['class'] === 'HTMLPurifier_Config') { + continue; + } + $frame = $trace[$i]; + $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; + break; + } + } + if ($no == E_USER_ERROR) { + throw new Exception($msg . $extra); + } else { + trigger_error($msg . $extra, $no); + } + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + * + * @return string + */ + public function serialize() + { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php new file mode 100644 index 0000000..42f6604 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php @@ -0,0 +1,176 @@ + array( + * 'Directive' => new stdClass(), + * ) + * ) + * + * The stdClass may have the following properties: + * + * - If isAlias isn't set: + * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions + * - allow_null: If set, this directive allows null values + * - aliases: If set, an associative array of value aliases to real values + * - allowed: If set, a lookup array of allowed (string) values + * - If isAlias is set: + * - namespace: Namespace this directive aliases to + * - name: Directive name this directive aliases to + * + * In certain degenerate cases, stdClass will actually be an integer. In + * that case, the value is equivalent to an stdClass with the type + * property set to the integer. If the integer is negative, type is + * equal to the absolute value of integer, and allow_null is true. + * + * This class is friendly with HTMLPurifier_Config. If you need introspection + * about the schema, you're better of using the ConfigSchema_Interchange, + * which uses more memory but has much richer information. + * @type array + */ + public $info = array(); + + /** + * Application-wide singleton + * @type HTMLPurifier_ConfigSchema + */ + protected static $singleton; + + public function __construct() + { + $this->defaultPlist = new HTMLPurifier_PropertyList(); + } + + /** + * Unserializes the default ConfigSchema. + * @return HTMLPurifier_ConfigSchema + */ + public static function makeFromSerial() + { + $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); + $r = unserialize($contents); + if (!$r) { + $hash = sha1($contents); + throw new Exception("Unserialization of configuration schema failed, sha1 of file was $hash"); + } + return $r; + } + + /** + * Retrieves an instance of the application-wide configuration definition. + * @param HTMLPurifier_ConfigSchema $prototype + * @return HTMLPurifier_ConfigSchema + */ + public static function instance($prototype = null) + { + if ($prototype !== null) { + HTMLPurifier_ConfigSchema::$singleton = $prototype; + } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { + HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); + } + return HTMLPurifier_ConfigSchema::$singleton; + } + + /** + * Defines a directive for configuration + * @warning Will fail of directive's namespace is defined. + * @warning This method's signature is slightly different from the legacy + * define() static method! Beware! + * @param string $key Name of directive + * @param mixed $default Default value of directive + * @param string $type Allowed type of the directive. See + * HTMLPurifier_VarParser::$types for allowed values + * @param bool $allow_null Whether or not to allow null values + */ + public function add($key, $default, $type, $allow_null) + { + $obj = new stdClass(); + $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; + if ($allow_null) { + $obj->allow_null = true; + } + $this->info[$key] = $obj; + $this->defaults[$key] = $default; + $this->defaultPlist->set($key, $default); + } + + /** + * Defines a directive value alias. + * + * Directive value aliases are convenient for developers because it lets + * them set a directive to several values and get the same result. + * @param string $key Name of Directive + * @param array $aliases Hash of aliased values to the real alias + */ + public function addValueAliases($key, $aliases) + { + if (!isset($this->info[$key]->aliases)) { + $this->info[$key]->aliases = array(); + } + foreach ($aliases as $alias => $real) { + $this->info[$key]->aliases[$alias] = $real; + } + } + + /** + * Defines a set of allowed values for a directive. + * @warning This is slightly different from the corresponding static + * method definition. + * @param string $key Name of directive + * @param array $allowed Lookup array of allowed values + */ + public function addAllowedValues($key, $allowed) + { + $this->info[$key]->allowed = $allowed; + } + + /** + * Defines a directive alias for backwards compatibility + * @param string $key Directive that will be aliased + * @param string $new_key Directive that the alias will be to + */ + public function addAlias($key, $new_key) + { + $obj = new stdClass; + $obj->key = $new_key; + $obj->isAlias = true; + $this->info[$key] = $obj; + } + + /** + * Replaces any stdClass that only has the type property with type integer. + */ + public function postProcess() + { + foreach ($this->info as $key => $v) { + if (count((array) $v) == 1) { + $this->info[$key] = $v->type; + } elseif (count((array) $v) == 2 && isset($v->allow_null)) { + $this->info[$key] = -$v->type; + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php new file mode 100644 index 0000000..d5906cd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -0,0 +1,48 @@ +directives as $d) { + $schema->add( + $d->id->key, + $d->default, + $d->type, + $d->typeAllowsNull + ); + if ($d->allowed !== null) { + $schema->addAllowedValues( + $d->id->key, + $d->allowed + ); + } + foreach ($d->aliases as $alias) { + $schema->addAlias( + $alias->key, + $d->id->key + ); + } + if ($d->valueAliases !== null) { + $schema->addValueAliases( + $d->id->key, + $d->valueAliases + ); + } + } + $schema->postProcess(); + return $schema; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php new file mode 100644 index 0000000..5fa56f7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -0,0 +1,144 @@ +startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + /** + * @param mixed $var + * @return string + */ + protected function export($var) + { + if ($var === array()) { + return 'array()'; + } + return var_export($var, true); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + */ + public function build($interchange) + { + // global access, only use as last resort + $this->interchange = $interchange; + + $this->setIndent(true); + $this->startDocument('1.0', 'UTF-8'); + $this->startElement('configdoc'); + $this->writeElement('title', $interchange->name); + + foreach ($interchange->directives as $directive) { + $this->buildDirective($directive); + } + + if ($this->namespace) { + $this->endElement(); + } // namespace + + $this->endElement(); // configdoc + $this->flush(); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + */ + public function buildDirective($directive) + { + // Kludge, although I suppose having a notion of a "root namespace" + // certainly makes things look nicer when documentation is built. + // Depends on things being sorted. + if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { + if ($this->namespace) { + $this->endElement(); + } // namespace + $this->namespace = $directive->id->getRootNamespace(); + $this->startElement('namespace'); + $this->writeAttribute('id', $this->namespace); + $this->writeElement('name', $this->namespace); + } + + $this->startElement('directive'); + $this->writeAttribute('id', $directive->id->toString()); + + $this->writeElement('name', $directive->id->getDirective()); + + $this->startElement('aliases'); + foreach ($directive->aliases as $alias) { + $this->writeElement('alias', $alias->toString()); + } + $this->endElement(); // aliases + + $this->startElement('constraints'); + if ($directive->version) { + $this->writeElement('version', $directive->version); + } + $this->startElement('type'); + if ($directive->typeAllowsNull) { + $this->writeAttribute('allow-null', 'yes'); + } + $this->text($directive->type); + $this->endElement(); // type + if ($directive->allowed) { + $this->startElement('allowed'); + foreach ($directive->allowed as $value => $x) { + $this->writeElement('value', $value); + } + $this->endElement(); // allowed + } + $this->writeElement('default', $this->export($directive->default)); + $this->writeAttribute('xml:space', 'preserve'); + if ($directive->external) { + $this->startElement('external'); + foreach ($directive->external as $project) { + $this->writeElement('project', $project); + } + $this->endElement(); + } + $this->endElement(); // constraints + + if ($directive->deprecatedVersion) { + $this->startElement('deprecated'); + $this->writeElement('version', $directive->deprecatedVersion); + $this->writeElement('use', $directive->deprecatedUse->toString()); + $this->endElement(); // deprecated + } + + $this->startElement('description'); + $this->writeHTMLDiv($directive->description); + $this->endElement(); // description + + $this->endElement(); // directive + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php new file mode 100644 index 0000000..2671516 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php @@ -0,0 +1,11 @@ + array(directive info) + * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] + */ + public $directives = array(); + + /** + * Adds a directive array to $directives + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function addDirective($directive) + { + if (isset($this->directives[$i = $directive->id->toString()])) { + throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); + } + $this->directives[$i] = $directive; + } + + /** + * Convenience function to perform standard validation. Throws exception + * on failed validation. + */ + public function validate() + { + $validator = new HTMLPurifier_ConfigSchema_Validator(); + return $validator->validate($this); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php new file mode 100644 index 0000000..4902a56 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -0,0 +1,89 @@ + true). + * Null if all values are allowed. + * @type array + */ + public $allowed; + + /** + * List of aliases for the directive. + * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). + * @type HTMLPurifier_ConfigSchema_Interchange_Id[] + */ + public $aliases = array(); + + /** + * Hash of value aliases, e.g. array('alt' => 'real'). Null if value + * aliasing is disabled (necessary for non-scalar types). + * @type array + */ + public $valueAliases; + + /** + * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. + * Null if the directive has always existed. + * @type string + */ + public $version; + + /** + * ID of directive that supersedes this old directive. + * Null if not deprecated. + * @type HTMLPurifier_ConfigSchema_Interchange_Id + */ + public $deprecatedUse; + + /** + * Version of HTML Purifier this directive was deprecated. Null if not + * deprecated. + * @type string + */ + public $deprecatedVersion; + + /** + * List of external projects this directive depends on, e.g. array('CSSTidy'). + * @type array + */ + public $external = array(); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php new file mode 100644 index 0000000..126f09d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -0,0 +1,58 @@ +key = $key; + } + + /** + * @return string + * @warning This is NOT magic, to ensure that people don't abuse SPL and + * cause problems for PHP 5.0 support. + */ + public function toString() + { + return $this->key; + } + + /** + * @return string + */ + public function getRootNamespace() + { + return substr($this->key, 0, strpos($this->key, ".")); + } + + /** + * @return string + */ + public function getDirective() + { + return substr($this->key, strpos($this->key, ".") + 1); + } + + /** + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + public static function make($id) + { + return new HTMLPurifier_ConfigSchema_Interchange_Id($id); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php new file mode 100644 index 0000000..655e6dd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -0,0 +1,226 @@ +varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + /** + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public static function buildFromDirectory($dir = null) + { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public function buildDir($interchange, $dir = null) + { + if (!$dir) { + $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; + } + if (file_exists($dir . '/info.ini')) { + $info = parse_ini_file($dir . '/info.ini'); + $interchange->name = $info['name']; + } + + $files = array(); + $dh = opendir($dir); + while (false !== ($file = readdir($dh))) { + if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { + continue; + } + $files[] = $file; + } + closedir($dh); + + sort($files); + foreach ($files as $file) { + $this->buildFile($interchange, $dir . '/' . $file); + } + return $interchange; + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $file + */ + public function buildFile($interchange, $file) + { + $parser = new HTMLPurifier_StringHashParser(); + $this->build( + $interchange, + new HTMLPurifier_StringHash($parser->parseFile($file)) + ); + } + + /** + * Builds an interchange object based on a hash. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param HTMLPurifier_StringHash $hash source data + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function build($interchange, $hash) + { + if (!$hash instanceof HTMLPurifier_StringHash) { + $hash = new HTMLPurifier_StringHash($hash); + } + if (!isset($hash['ID'])) { + throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); + } + if (strpos($hash['ID'], '.') === false) { + if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { + $hash->offsetGet('DESCRIPTION'); // prevent complaining + } else { + throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); + } + } else { + $this->buildDirective($interchange, $hash); + } + $this->_findUnused($hash); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param HTMLPurifier_StringHash $hash + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function buildDirective($interchange, $hash) + { + $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); + + // These are required elements: + $directive->id = $this->id($hash->offsetGet('ID')); + $id = $directive->id->toString(); // convenience + + if (isset($hash['TYPE'])) { + $type = explode('/', $hash->offsetGet('TYPE')); + if (isset($type[1])) { + $directive->typeAllowsNull = true; + } + $directive->type = $type[0]; + } else { + throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); + } + + if (isset($hash['DEFAULT'])) { + try { + $directive->default = $this->varParser->parse( + $hash->offsetGet('DEFAULT'), + $directive->type, + $directive->typeAllowsNull + ); + } catch (HTMLPurifier_VarParserException $e) { + throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); + } + } + + if (isset($hash['DESCRIPTION'])) { + $directive->description = $hash->offsetGet('DESCRIPTION'); + } + + if (isset($hash['ALLOWED'])) { + $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); + } + + if (isset($hash['VALUE-ALIASES'])) { + $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); + } + + if (isset($hash['ALIASES'])) { + $raw_aliases = trim($hash->offsetGet('ALIASES')); + $aliases = preg_split('/\s*,\s*/', $raw_aliases); + foreach ($aliases as $alias) { + $directive->aliases[] = $this->id($alias); + } + } + + if (isset($hash['VERSION'])) { + $directive->version = $hash->offsetGet('VERSION'); + } + + if (isset($hash['DEPRECATED-USE'])) { + $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); + } + + if (isset($hash['DEPRECATED-VERSION'])) { + $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); + } + + if (isset($hash['EXTERNAL'])) { + $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); + } + + $interchange->addDirective($directive); + } + + /** + * Evaluates an array PHP code string without array() wrapper + * @param string $contents + */ + protected function evalArray($contents) + { + return eval('return array(' . $contents . ');'); + } + + /** + * Converts an array list into a lookup array. + * @param array $array + * @return array + */ + protected function lookup($array) + { + $ret = array(); + foreach ($array as $val) { + $ret[$val] = true; + } + return $ret; + } + + /** + * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id + * object based on a string Id. + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + protected function id($id) + { + return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); + } + + /** + * Triggers errors for any unused keys passed in the hash; such keys + * may indicate typos, missing values, etc. + * @param HTMLPurifier_StringHash $hash Hash to check. + */ + protected function _findUnused($hash) + { + $accessed = $hash->getAccessed(); + foreach ($hash as $k => $v) { + if (!isset($accessed[$k])) { + trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php new file mode 100644 index 0000000..fb31277 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php @@ -0,0 +1,248 @@ +parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return bool + */ + public function validate($interchange) + { + $this->interchange = $interchange; + $this->aliases = array(); + // PHP is a bit lax with integer <=> string conversions in + // arrays, so we don't use the identical !== comparison + foreach ($interchange->directives as $i => $directive) { + $id = $directive->id->toString(); + if ($i != $id) { + $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); + } + $this->validateDirective($directive); + } + return true; + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. + * @param HTMLPurifier_ConfigSchema_Interchange_Id $id + */ + public function validateId($id) + { + $id_string = $id->toString(); + $this->context[] = "id '$id_string'"; + if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { + // handled by InterchangeBuilder + $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); + } + // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) + // we probably should check that it has at least one namespace + $this->with($id, 'key') + ->assertNotEmpty() + ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder + array_pop($this->context); + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirective($d) + { + $id = $d->id->toString(); + $this->context[] = "directive '$id'"; + $this->validateId($d->id); + + $this->with($d, 'description') + ->assertNotEmpty(); + + // BEGIN - handled by InterchangeBuilder + $this->with($d, 'type') + ->assertNotEmpty(); + $this->with($d, 'typeAllowsNull') + ->assertIsBool(); + try { + // This also tests validity of $d->type + $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + $this->error('default', 'had error: ' . $e->getMessage()); + } + // END - handled by InterchangeBuilder + + if (!is_null($d->allowed) || !empty($d->valueAliases)) { + // allowed and valueAliases require that we be dealing with + // strings, so check for that early. + $d_int = HTMLPurifier_VarParser::$types[$d->type]; + if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { + $this->error('type', 'must be a string type when used with allowed or value aliases'); + } + } + + $this->validateDirectiveAllowed($d); + $this->validateDirectiveValueAliases($d); + $this->validateDirectiveAliases($d); + + array_pop($this->context); + } + + /** + * Extra validation if $allowed member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAllowed($d) + { + if (is_null($d->allowed)) { + return; + } + $this->with($d, 'allowed') + ->assertNotEmpty() + ->assertIsLookup(); // handled by InterchangeBuilder + if (is_string($d->default) && !isset($d->allowed[$d->default])) { + $this->error('default', 'must be an allowed value'); + } + $this->context[] = 'allowed'; + foreach ($d->allowed as $val => $x) { + if (!is_string($val)) { + $this->error("value $val", 'must be a string'); + } + } + array_pop($this->context); + } + + /** + * Extra validation if $valueAliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveValueAliases($d) + { + if (is_null($d->valueAliases)) { + return; + } + $this->with($d, 'valueAliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'valueAliases'; + foreach ($d->valueAliases as $alias => $real) { + if (!is_string($alias)) { + $this->error("alias $alias", 'must be a string'); + } + if (!is_string($real)) { + $this->error("alias target $real from alias '$alias'", 'must be a string'); + } + if ($alias === $real) { + $this->error("alias '$alias'", "must not be an alias to itself"); + } + } + if (!is_null($d->allowed)) { + foreach ($d->valueAliases as $alias => $real) { + if (isset($d->allowed[$alias])) { + $this->error("alias '$alias'", 'must not be an allowed value'); + } elseif (!isset($d->allowed[$real])) { + $this->error("alias '$alias'", 'must be an alias to an allowed value'); + } + } + } + array_pop($this->context); + } + + /** + * Extra validation if $aliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAliases($d) + { + $this->with($d, 'aliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'aliases'; + foreach ($d->aliases as $alias) { + $this->validateId($alias); + $s = $alias->toString(); + if (isset($this->interchange->directives[$s])) { + $this->error("alias '$s'", 'collides with another directive'); + } + if (isset($this->aliases[$s])) { + $other_directive = $this->aliases[$s]; + $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); + } + $this->aliases[$s] = $d->id->toString(); + } + array_pop($this->context); + } + + // protected helper functions + + /** + * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom + * for validating simple member variables of objects. + * @param $obj + * @param $member + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + protected function with($obj, $member) + { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($target, $msg) + { + if ($target !== false) { + $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); + } else { + $prefix = ucfirst($this->getFormattedContext()); + } + throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); + } + + /** + * Returns a formatted context string. + * @return string + */ + protected function getFormattedContext() + { + return implode(' in ', array_reverse($this->context)); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php new file mode 100644 index 0000000..c9aa364 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -0,0 +1,130 @@ +context = $context; + $this->obj = $obj; + $this->member = $member; + $this->contents =& $obj->$member; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsString() + { + if (!is_string($this->contents)) { + $this->error('must be a string'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsBool() + { + if (!is_bool($this->contents)) { + $this->error('must be a boolean'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsArray() + { + if (!is_array($this->contents)) { + $this->error('must be an array'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotNull() + { + if ($this->contents === null) { + $this->error('must not be null'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertAlnum() + { + $this->assertIsString(); + if (!ctype_alnum($this->contents)) { + $this->error('must be alphanumeric'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotEmpty() + { + if (empty($this->contents)) { + $this->error('must not be empty'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsLookup() + { + $this->assertIsArray(); + foreach ($this->contents as $v) { + if ($v !== true) { + $this->error('must be a lookup array'); + } + } + return $this; + } + + /** + * @param string $msg + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($msg) + { + throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser new file mode 100644 index 0000000..34ea683 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser @@ -0,0 +1 @@ +O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:130:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";N;s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:17:"Core.RemoveBlanks";b:0;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";N;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:18:"URI.AllowedSymbols";s:11:"!$&'()*+,;=";s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:19:"URI.SafeIframeHosts";N;s:20:"URI.SafeIframeRegexp";N;}s:12:"defaultPlist";O:25:"HTMLPurifier_PropertyList":3:{s:7:"*data";a:130:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";N;s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:17:"Core.RemoveBlanks";b:0;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";N;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:18:"URI.AllowedSymbols";s:11:"!$&'()*+,;=";s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:19:"URI.SafeIframeHosts";N;s:20:"URI.SafeIframeRegexp";N;}s:9:"*parent";N;s:8:"*cache";N;}s:4:"info";a:143:{s:19:"Attr.AllowedClasses";i:-8;s:24:"Attr.AllowedFrameTargets";i:8;s:15:"Attr.AllowedRel";i:8;s:15:"Attr.AllowedRev";i:8;s:18:"Attr.ClassUseCDATA";i:-7;s:20:"Attr.DefaultImageAlt";i:-1;s:24:"Attr.DefaultInvalidImage";i:1;s:27:"Attr.DefaultInvalidImageAlt";i:1;s:19:"Attr.DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:13:"Attr.EnableID";i:7;s:17:"HTML.EnableAttrID";O:8:"stdClass":2:{s:3:"key";s:13:"Attr.EnableID";s:7:"isAlias";b:1;}s:21:"Attr.ForbiddenClasses";i:8;s:13:"Attr.ID.HTML5";i:-7;s:16:"Attr.IDBlacklist";i:9;s:22:"Attr.IDBlacklistRegexp";i:-1;s:13:"Attr.IDPrefix";i:1;s:18:"Attr.IDPrefixLocal";i:1;s:24:"AutoFormat.AutoParagraph";i:7;s:17:"AutoFormat.Custom";i:9;s:25:"AutoFormat.DisplayLinkURI";i:7;s:18:"AutoFormat.Linkify";i:7;s:33:"AutoFormat.PurifierLinkify.DocURL";i:1;s:37:"AutoFormatParam.PurifierLinkifyDocURL";O:8:"stdClass":2:{s:3:"key";s:33:"AutoFormat.PurifierLinkify.DocURL";s:7:"isAlias";b:1;}s:26:"AutoFormat.PurifierLinkify";i:7;s:32:"AutoFormat.RemoveEmpty.Predicate";i:10;s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";i:8;s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";i:7;s:22:"AutoFormat.RemoveEmpty";i:7;s:39:"AutoFormat.RemoveSpansWithoutAttributes";i:7;s:19:"CSS.AllowDuplicates";i:7;s:18:"CSS.AllowImportant";i:7;s:15:"CSS.AllowTricky";i:7;s:16:"CSS.AllowedFonts";i:-8;s:21:"CSS.AllowedProperties";i:-8;s:17:"CSS.DefinitionRev";i:5;s:23:"CSS.ForbiddenProperties";i:8;s:16:"CSS.MaxImgLength";i:-1;s:15:"CSS.Proprietary";i:7;s:11:"CSS.Trusted";i:7;s:20:"Cache.DefinitionImpl";i:-1;s:20:"Core.DefinitionCache";O:8:"stdClass":2:{s:3:"key";s:20:"Cache.DefinitionImpl";s:7:"isAlias";b:1;}s:20:"Cache.SerializerPath";i:-1;s:27:"Cache.SerializerPermissions";i:-5;s:22:"Core.AggressivelyFixLt";i:7;s:29:"Core.AggressivelyRemoveScript";i:7;s:28:"Core.AllowHostnameUnderscore";i:7;s:23:"Core.AllowParseManyTags";i:7;s:18:"Core.CollectErrors";i:7;s:18:"Core.ColorKeywords";i:10;s:30:"Core.ConvertDocumentToFragment";i:7;s:24:"Core.AcceptFullDocuments";O:8:"stdClass":2:{s:3:"key";s:30:"Core.ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:36:"Core.DirectLexLineNumberSyncInterval";i:5;s:20:"Core.DisableExcludes";i:7;s:15:"Core.EnableIDNA";i:7;s:13:"Core.Encoding";i:2;s:26:"Core.EscapeInvalidChildren";i:7;s:22:"Core.EscapeInvalidTags";i:7;s:29:"Core.EscapeNonASCIICharacters";i:7;s:19:"Core.HiddenElements";i:8;s:13:"Core.Language";i:1;s:24:"Core.LegacyEntityDecoder";i:7;s:14:"Core.LexerImpl";i:-11;s:24:"Core.MaintainLineNumbers";i:-7;s:22:"Core.NormalizeNewlines";i:7;s:17:"Core.RemoveBlanks";i:7;s:21:"Core.RemoveInvalidImg";i:7;s:33:"Core.RemoveProcessingInstructions";i:7;s:25:"Core.RemoveScriptContents";i:-7;s:13:"Filter.Custom";i:9;s:34:"Filter.ExtractStyleBlocks.Escaping";i:7;s:33:"Filter.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:38:"FilterParam.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:31:"Filter.ExtractStyleBlocks.Scope";i:-1;s:30:"Filter.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:35:"FilterParam.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:34:"Filter.ExtractStyleBlocks.TidyImpl";i:-11;s:38:"FilterParam.ExtractStyleBlocksTidyImpl";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.TidyImpl";s:7:"isAlias";b:1;}s:25:"Filter.ExtractStyleBlocks";i:7;s:14:"Filter.YouTube";i:7;s:12:"HTML.Allowed";i:-4;s:22:"HTML.AllowedAttributes";i:-8;s:20:"HTML.AllowedComments";i:8;s:26:"HTML.AllowedCommentsRegexp";i:-1;s:20:"HTML.AllowedElements";i:-8;s:19:"HTML.AllowedModules";i:-8;s:23:"HTML.Attr.Name.UseCDATA";i:7;s:17:"HTML.BlockWrapper";i:1;s:16:"HTML.CoreModules";i:8;s:18:"HTML.CustomDoctype";i:-1;s:17:"HTML.DefinitionID";i:-1;s:18:"HTML.DefinitionRev";i:5;s:12:"HTML.Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:25:"HTML.FlashAllowFullScreen";i:7;s:24:"HTML.ForbiddenAttributes";i:8;s:22:"HTML.ForbiddenElements";i:8;s:10:"HTML.Forms";i:7;s:17:"HTML.MaxImgLength";i:-5;s:13:"HTML.Nofollow";i:7;s:11:"HTML.Parent";i:1;s:16:"HTML.Proprietary";i:7;s:14:"HTML.SafeEmbed";i:7;s:15:"HTML.SafeIframe";i:7;s:15:"HTML.SafeObject";i:7;s:18:"HTML.SafeScripting";i:8;s:11:"HTML.Strict";i:7;s:16:"HTML.TargetBlank";i:7;s:19:"HTML.TargetNoopener";i:7;s:21:"HTML.TargetNoreferrer";i:7;s:12:"HTML.TidyAdd";i:8;s:14:"HTML.TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:15:"HTML.TidyRemove";i:8;s:12:"HTML.Trusted";i:7;s:10:"HTML.XHTML";i:7;s:10:"Core.XHTML";O:8:"stdClass":2:{s:3:"key";s:10:"HTML.XHTML";s:7:"isAlias";b:1;}s:28:"Output.CommentScriptContents";i:7;s:26:"Core.CommentScriptContents";O:8:"stdClass":2:{s:3:"key";s:28:"Output.CommentScriptContents";s:7:"isAlias";b:1;}s:19:"Output.FixInnerHTML";i:7;s:18:"Output.FlashCompat";i:7;s:14:"Output.Newline";i:-1;s:15:"Output.SortAttr";i:7;s:17:"Output.TidyFormat";i:7;s:15:"Core.TidyFormat";O:8:"stdClass":2:{s:3:"key";s:17:"Output.TidyFormat";s:7:"isAlias";b:1;}s:17:"Test.ForceNoIconv";i:7;s:18:"URI.AllowedSchemes";i:8;s:18:"URI.AllowedSymbols";i:-1;s:8:"URI.Base";i:-1;s:17:"URI.DefaultScheme";i:-1;s:16:"URI.DefinitionID";i:-1;s:17:"URI.DefinitionRev";i:5;s:11:"URI.Disable";i:7;s:15:"Attr.DisableURI";O:8:"stdClass":2:{s:3:"key";s:11:"URI.Disable";s:7:"isAlias";b:1;}s:19:"URI.DisableExternal";i:7;s:28:"URI.DisableExternalResources";i:7;s:20:"URI.DisableResources";i:7;s:8:"URI.Host";i:-1;s:17:"URI.HostBlacklist";i:9;s:16:"URI.MakeAbsolute";i:7;s:9:"URI.Munge";i:-1;s:18:"URI.MungeResources";i:7;s:18:"URI.MungeSecretKey";i:-1;s:26:"URI.OverrideAllowedSchemes";i:7;s:19:"URI.SafeIframeHosts";i:-8;s:20:"URI.SafeIframeRegexp";i:-1;}} \ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt new file mode 100644 index 0000000..0517fed --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt @@ -0,0 +1,8 @@ +Attr.AllowedClasses +TYPE: lookup/null +VERSION: 4.0.0 +DEFAULT: null +--DESCRIPTION-- +List of allowed class values in the class attribute. By default, this is null, +which means all classes are allowed. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt new file mode 100644 index 0000000..249edd6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt @@ -0,0 +1,12 @@ +Attr.AllowedFrameTargets +TYPE: lookup +DEFAULT: array() +--DESCRIPTION-- +Lookup table of all allowed link frame targets. Some commonly used link +targets include _blank, _self, _parent and _top. Values should be +lowercase, as validation will be done in a case-sensitive manner despite +W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute +so this directive will have no effect in that doctype. XHTML 1.1 does not +enable the Target module by default, you will have to manually enable it +(see the module documentation for more details.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt new file mode 100644 index 0000000..9a8fa6a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt @@ -0,0 +1,9 @@ +Attr.AllowedRel +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed forward document relationships in the rel attribute. Common +values may be nofollow or print. By default, this is empty, meaning that no +document relationships are allowed. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt new file mode 100644 index 0000000..b017883 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt @@ -0,0 +1,9 @@ +Attr.AllowedRev +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed reverse document relationships in the rev attribute. This +attribute is a bit of an edge-case; if you don't know what it is for, stay +away. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt new file mode 100644 index 0000000..e774b82 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt @@ -0,0 +1,19 @@ +Attr.ClassUseCDATA +TYPE: bool/null +DEFAULT: null +VERSION: 4.0.0 +--DESCRIPTION-- +If null, class will auto-detect the doctype and, if matching XHTML 1.1 or +XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, +it will use a relaxed CDATA definition. If true, the relaxed CDATA definition +is forced; if false, the NMTOKENS definition is forced. To get behavior +of HTML Purifier prior to 4.0.0, set this directive to false. + +Some rational behind the auto-detection: +in previous versions of HTML Purifier, it was assumed that the form of +class was NMTOKENS, as specified by the XHTML Modularization (representing +XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however +specify class as CDATA. HTML 5 effectively defines it as CDATA, but +with the additional constraint that each name should be unique (this is not +explicitly outlined in previous specifications). +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt new file mode 100644 index 0000000..533165e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt @@ -0,0 +1,11 @@ +Attr.DefaultImageAlt +TYPE: string/null +DEFAULT: null +VERSION: 3.2.0 +--DESCRIPTION-- +This is the content of the alt tag of an image if the user had not +previously specified an alt attribute. This applies to all images without +a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which +only applies to invalid images, and overrides in the case of an invalid image. +Default behavior with null is to use the basename of the src tag for the alt. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt new file mode 100644 index 0000000..9eb7e38 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt @@ -0,0 +1,9 @@ +Attr.DefaultInvalidImage +TYPE: string +DEFAULT: '' +--DESCRIPTION-- +This is the default image an img tag will be pointed to if it does not have +a valid src attribute. In future versions, we may allow the image tag to +be removed completely, but due to design issues, this is not possible right +now. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt new file mode 100644 index 0000000..2f17bf4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt @@ -0,0 +1,8 @@ +Attr.DefaultInvalidImageAlt +TYPE: string +DEFAULT: 'Invalid image' +--DESCRIPTION-- +This is the content of the alt tag of an invalid image if the user had not +previously specified an alt attribute. It has no effect when the image is +valid but there was no alt attribute present. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt new file mode 100644 index 0000000..52654b5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt @@ -0,0 +1,10 @@ +Attr.DefaultTextDir +TYPE: string +DEFAULT: 'ltr' +--DESCRIPTION-- +Defines the default text direction (ltr or rtl) of the document being +parsed. This generally is the same as the value of the dir attribute in +HTML, or ltr if that is not specified. +--ALLOWED-- +'ltr', 'rtl' +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt new file mode 100644 index 0000000..6440d21 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt @@ -0,0 +1,16 @@ +Attr.EnableID +TYPE: bool +DEFAULT: false +VERSION: 1.2.0 +--DESCRIPTION-- +Allows the ID attribute in HTML. This is disabled by default due to the +fact that without proper configuration user input can easily break the +validation of a webpage by specifying an ID that is already on the +surrounding HTML. If you don't mind throwing caution to the wind, enable +this directive, but I strongly recommend you also consider blacklisting IDs +you use (%Attr.IDBlacklist) or prefixing all user supplied IDs +(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of +pre-1.2.0 versions. +--ALIASES-- +HTML.EnableAttrID +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt new file mode 100644 index 0000000..f31d226 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt @@ -0,0 +1,8 @@ +Attr.ForbiddenClasses +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array() +--DESCRIPTION-- +List of forbidden class values in the class attribute. By default, this is +empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt new file mode 100644 index 0000000..735d4b7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt @@ -0,0 +1,10 @@ +Attr.ID.HTML5 +TYPE: bool/null +DEFAULT: null +VERSION: 4.8.0 +--DESCRIPTION-- +In HTML5, restrictions on the format of the id attribute have been significantly +relaxed, such that any string is valid so long as it contains no spaces and +is at least one character. In lieu of a general HTML5 compatibility flag, +set this configuration directive to true to use the relaxed rules. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt new file mode 100644 index 0000000..5f2b5e3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt @@ -0,0 +1,5 @@ +Attr.IDBlacklist +TYPE: list +DEFAULT: array() +DESCRIPTION: Array of IDs not allowed in the document. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt new file mode 100644 index 0000000..6f58245 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt @@ -0,0 +1,9 @@ +Attr.IDBlacklistRegexp +TYPE: string/null +VERSION: 1.6.0 +DEFAULT: NULL +--DESCRIPTION-- +PCRE regular expression to be matched against all IDs. If the expression is +matches, the ID is rejected. Use this with care: may cause significant +degradation. ID matching is done after all other validation. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt new file mode 100644 index 0000000..cc49d43 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt @@ -0,0 +1,12 @@ +Attr.IDPrefix +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +String to prefix to IDs. If you have no idea what IDs your pages may use, +you may opt to simply add a prefix to all user-submitted ID attributes so +that they are still usable, but will not conflict with core page IDs. +Example: setting the directive to 'user_' will result in a user submitted +'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true +before using this. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt new file mode 100644 index 0000000..dc6e30f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt @@ -0,0 +1,14 @@ +Attr.IDPrefixLocal +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you +need to allow multiple sets of user content on web page, you may need to +have a separate prefix that changes with each iteration. This way, +separately submitted user content displayed on the same page doesn't +clobber each other. Ideal values are unique identifiers for the content it +represents (i.e. the id of the row in the database). Be sure to add a +separator (like an underscore) at the end. Warning: this directive will +not work unless %Attr.IDPrefix is set to a non-empty value! +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt new file mode 100644 index 0000000..d5caa1b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt @@ -0,0 +1,31 @@ +AutoFormat.AutoParagraph +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ This directive turns on auto-paragraphing, where double newlines are + converted in to paragraphs whenever possible. Auto-paragraphing: +

+
    +
  • Always applies to inline elements or text in the root node,
  • +
  • Applies to inline elements or text with double newlines in nodes + that allow paragraph tags,
  • +
  • Applies to double newlines in paragraph tags
  • +
+

+ p tags must be allowed for this directive to take effect. + We do not use br tags for paragraphing, as that is + semantically incorrect. +

+

+ To prevent auto-paragraphing as a content-producer, refrain from using + double-newlines except to specify a new paragraph or in contexts where + it has special meaning (whitespace usually has no meaning except in + tags like pre, so this should not be difficult.) To prevent + the paragraphing of inline text adjacent to block elements, wrap them + in div tags (the behavior is slightly different outside of + the root node.) +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt new file mode 100644 index 0000000..2a47648 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt @@ -0,0 +1,12 @@ +AutoFormat.Custom +TYPE: list +VERSION: 2.0.1 +DEFAULT: array() +--DESCRIPTION-- + +

+ This directive can be used to add custom auto-format injectors. + Specify an array of injector names (class name minus the prefix) + or concrete implementations. Injector class must exist. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt new file mode 100644 index 0000000..663064a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt @@ -0,0 +1,11 @@ +AutoFormat.DisplayLinkURI +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ This directive turns on the in-text display of URIs in <a> tags, and disables + those links. For example, example becomes + example (http://example.com). +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt new file mode 100644 index 0000000..3a48ba9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt @@ -0,0 +1,12 @@ +AutoFormat.Linkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ This directive turns on linkification, auto-linking http, ftp and + https URLs. a tags with the href attribute + must be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt new file mode 100644 index 0000000..db58b13 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify.DocURL +TYPE: string +VERSION: 2.0.1 +DEFAULT: '#%s' +ALIASES: AutoFormatParam.PurifierLinkifyDocURL +--DESCRIPTION-- +

+ Location of configuration documentation to link to, let %s substitute + into the configuration's namespace and directive names sans the percent + sign. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt new file mode 100644 index 0000000..7996488 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ Internal auto-formatter that converts configuration directives in + syntax %Namespace.Directive to links. a tags + with the href attribute must be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt new file mode 100644 index 0000000..6367fe2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt @@ -0,0 +1,14 @@ +AutoFormat.RemoveEmpty.Predicate +TYPE: hash +VERSION: 4.7.0 +DEFAULT: array('colgroup' => array(), 'th' => array(), 'td' => array(), 'iframe' => array('src')) +--DESCRIPTION-- +

+ Given that an element has no contents, it will be removed by default, unless + this predicate dictates otherwise. The predicate can either be an associative + map from tag name to list of attributes that must be present for the element + to be considered preserved: thus, the default always preserves colgroup, + th and td, and also iframe if it + has a src. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt new file mode 100644 index 0000000..35c393b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array('td' => true, 'th' => true) +--DESCRIPTION-- +

+ When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp + are enabled, this directive defines what HTML elements should not be + removede if they have only a non-breaking space in them. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt new file mode 100644 index 0000000..9228dee --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt @@ -0,0 +1,15 @@ +AutoFormat.RemoveEmpty.RemoveNbsp +TYPE: bool +VERSION: 4.0.0 +DEFAULT: false +--DESCRIPTION-- +

+ When enabled, HTML Purifier will treat any elements that contain only + non-breaking spaces as well as regular whitespace as empty, and remove + them when %AutoFormat.RemoveEmpty is enabled. +

+

+ See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements + that don't have this behavior applied to them. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt new file mode 100644 index 0000000..34657ba --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt @@ -0,0 +1,46 @@ +AutoFormat.RemoveEmpty +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ When enabled, HTML Purifier will attempt to remove empty elements that + contribute no semantic information to the document. The following types + of nodes will be removed: +

+
  • + Tags with no attributes and no content, and that are not empty + elements (remove <a></a> but not + <br />), and +
  • +
  • + Tags with no content, except for:
      +
    • The colgroup element, or
    • +
    • + Elements with the id or name attribute, + when those attributes are permitted on those elements. +
    • +
  • +
+

+ Please be very careful when using this functionality; while it may not + seem that empty elements contain useful information, they can alter the + layout of a document given appropriate styling. This directive is most + useful when you are processing machine-generated HTML, please avoid using + it on regular user HTML. +

+

+ Elements that contain only whitespace will be treated as empty. Non-breaking + spaces, however, do not count as whitespace. See + %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. +

+

+ This algorithm is not perfect; you may still notice some empty tags, + particularly if a node had elements, but those elements were later removed + because they were not permitted in that context, or tags that, after + being auto-closed by another tag, where empty. This is for safety reasons + to prevent clever code from breaking validation. The general rule of thumb: + if a tag looked empty on the way in, it will get removed; if HTML Purifier + made it empty, it will stay. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt new file mode 100644 index 0000000..dde990a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveSpansWithoutAttributes +TYPE: bool +VERSION: 4.0.1 +DEFAULT: false +--DESCRIPTION-- +

+ This directive causes span tags without any attributes + to be removed. It will also remove spans that had all attributes + removed during processing. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt new file mode 100644 index 0000000..4d054b1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt @@ -0,0 +1,11 @@ +CSS.AllowDuplicates +TYPE: bool +DEFAULT: false +VERSION: 4.8.0 +--DESCRIPTION-- +

+ By default, HTML Purifier removes duplicate CSS properties, + like color:red; color:blue. If this is set to + true, duplicate properties are allowed. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt new file mode 100644 index 0000000..b324608 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt @@ -0,0 +1,8 @@ +CSS.AllowImportant +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not !important cascade modifiers should +be allowed in user CSS. If false, !important will stripped. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt new file mode 100644 index 0000000..748be0e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt @@ -0,0 +1,11 @@ +CSS.AllowTricky +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not to allow "tricky" CSS properties and +values. Tricky CSS properties/values can drastically modify page layout or +be used for deceptive practices but do not directly constitute a security risk. +For example, display:none; is considered a tricky property that +will only be allowed if this directive is set to true. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt new file mode 100644 index 0000000..3fd4654 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt @@ -0,0 +1,12 @@ +CSS.AllowedFonts +TYPE: lookup/null +VERSION: 4.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ Allows you to manually specify a set of allowed fonts. If + NULL, all fonts are allowed. This directive + affects generic names (serif, sans-serif, monospace, cursive, + fantasy) as well as specific font families. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt new file mode 100644 index 0000000..460112e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt @@ -0,0 +1,18 @@ +CSS.AllowedProperties +TYPE: lookup/null +VERSION: 3.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If HTML Purifier's style attributes set is unsatisfactory for your needs, + you can overload it with your own list of tags to allow. Note that this + method is subtractive: it does its job by taking away from HTML Purifier + usual feature set, so you cannot add an attribute that HTML Purifier never + supported in the first place. +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt new file mode 100644 index 0000000..5cb7dda --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt @@ -0,0 +1,11 @@ +CSS.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt new file mode 100644 index 0000000..f1f5c5f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt @@ -0,0 +1,13 @@ +CSS.ForbiddenProperties +TYPE: lookup +VERSION: 4.2.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This is the logical inverse of %CSS.AllowedProperties, and it will + override that directive or any other directive. If possible, + %CSS.AllowedProperties is recommended over this directive, + because it can sometimes be difficult to tell whether or not you've + forbidden all of the CSS properties you truly would like to disallow. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt new file mode 100644 index 0000000..63c2730 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -0,0 +1,16 @@ +CSS.MaxImgLength +TYPE: string/null +DEFAULT: null +VERSION: 3.1.1 +--DESCRIPTION-- +

+ This parameter sets the maximum allowed length on img tags, + effectively the width and height properties. + Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %HTML.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the CSS max is a number with + a unit). +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt new file mode 100644 index 0000000..148eedb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt @@ -0,0 +1,10 @@ +CSS.Proprietary +TYPE: bool +VERSION: 3.0.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Whether or not to allow safe, proprietary CSS values. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt new file mode 100644 index 0000000..e733a61 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt @@ -0,0 +1,9 @@ +CSS.Trusted +TYPE: bool +VERSION: 4.2.1 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user's CSS input is trusted or not. If the +input is trusted, a more expansive set of allowed properties. See +also %HTML.Trusted. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt new file mode 100644 index 0000000..c486724 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt @@ -0,0 +1,14 @@ +Cache.DefinitionImpl +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: 'Serializer' +--DESCRIPTION-- + +This directive defines which method to use when caching definitions, +the complex data-type that makes HTML Purifier tick. Set to null +to disable caching (not recommended, as you will see a definite +performance degradation). + +--ALIASES-- +Core.DefinitionCache +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt new file mode 100644 index 0000000..5403650 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt @@ -0,0 +1,13 @@ +Cache.SerializerPath +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Absolute path with no trailing slash to store serialized definitions in. + Default is within the + HTML Purifier library inside DefinitionCache/Serializer. This + path must be writable by the webserver. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt new file mode 100644 index 0000000..2e0cc81 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt @@ -0,0 +1,16 @@ +Cache.SerializerPermissions +TYPE: int/null +VERSION: 4.3.0 +DEFAULT: 0755 +--DESCRIPTION-- + +

+ Directory permissions of the files and directories created inside + the DefinitionCache/Serializer or other custom serializer path. +

+

+ In HTML Purifier 4.8.0, this also supports NULL, + which means that no chmod'ing or directory creation shall + occur. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt new file mode 100644 index 0000000..568cbf3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt @@ -0,0 +1,18 @@ +Core.AggressivelyFixLt +TYPE: bool +VERSION: 2.1.0 +DEFAULT: true +--DESCRIPTION-- +

+ This directive enables aggressive pre-filter fixes HTML Purifier can + perform in order to ensure that open angled-brackets do not get killed + during parsing stage. Enabling this will result in two preg_replace_callback + calls and at least two preg_replace calls for every HTML document parsed; + if your users make very well-formed HTML, you can set this directive false. + This has no effect when DirectLex is used. +

+

+ Notice: This directive's default turned from false to true + in HTML Purifier 3.2.0. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt new file mode 100644 index 0000000..b2b6ab1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt @@ -0,0 +1,16 @@ +Core.AggressivelyRemoveScript +TYPE: bool +VERSION: 4.9.0 +DEFAULT: true +--DESCRIPTION-- +

+ This directive enables aggressive pre-filter removal of + script tags. This is not necessary for security, + but it can help work around a bug in libxml where embedded + HTML elements inside script sections cause the parser to + choke. To revert to pre-4.9.0 behavior, set this to false. + This directive has no effect if %Core.Trusted is true, + %Core.RemoveScriptContents is false, or %Core.HiddenElements + does not contain script. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt new file mode 100644 index 0000000..2c910cc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt @@ -0,0 +1,16 @@ +Core.AllowHostnameUnderscore +TYPE: bool +VERSION: 4.6.0 +DEFAULT: false +--DESCRIPTION-- +

+ By RFC 1123, underscores are not permitted in host names. + (This is in contrast to the specification for DNS, RFC + 2181, which allows underscores.) + However, most browsers do the right thing when faced with + an underscore in the host name, and so some poorly written + websites are written with the expectation this should work. + Setting this parameter to true relaxes our allowed character + check so that underscores are permitted. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt new file mode 100644 index 0000000..06278f8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt @@ -0,0 +1,12 @@ +Core.AllowParseManyTags +TYPE: bool +DEFAULT: false +VERSION: 4.10.1 +--DESCRIPTION-- +

+ This directive allows parsing of many nested tags. + If you set true, relaxes any hardcoded limit from the parser. + However, in that case it may cause a Dos attack. + Be careful when enabling it. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt new file mode 100644 index 0000000..d731791 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt @@ -0,0 +1,12 @@ +Core.CollectErrors +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- + +Whether or not to collect errors found while filtering the document. This +is a useful way to give feedback to your users. Warning: +Currently this feature is very patchy and experimental, with lots of +possible error messages not yet implemented. It will not cause any +problems, but it may not help your users either. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt new file mode 100644 index 0000000..a75844c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt @@ -0,0 +1,160 @@ +Core.ColorKeywords +TYPE: hash +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'aliceblue' => '#F0F8FF', + 'antiquewhite' => '#FAEBD7', + 'aqua' => '#00FFFF', + 'aquamarine' => '#7FFFD4', + 'azure' => '#F0FFFF', + 'beige' => '#F5F5DC', + 'bisque' => '#FFE4C4', + 'black' => '#000000', + 'blanchedalmond' => '#FFEBCD', + 'blue' => '#0000FF', + 'blueviolet' => '#8A2BE2', + 'brown' => '#A52A2A', + 'burlywood' => '#DEB887', + 'cadetblue' => '#5F9EA0', + 'chartreuse' => '#7FFF00', + 'chocolate' => '#D2691E', + 'coral' => '#FF7F50', + 'cornflowerblue' => '#6495ED', + 'cornsilk' => '#FFF8DC', + 'crimson' => '#DC143C', + 'cyan' => '#00FFFF', + 'darkblue' => '#00008B', + 'darkcyan' => '#008B8B', + 'darkgoldenrod' => '#B8860B', + 'darkgray' => '#A9A9A9', + 'darkgrey' => '#A9A9A9', + 'darkgreen' => '#006400', + 'darkkhaki' => '#BDB76B', + 'darkmagenta' => '#8B008B', + 'darkolivegreen' => '#556B2F', + 'darkorange' => '#FF8C00', + 'darkorchid' => '#9932CC', + 'darkred' => '#8B0000', + 'darksalmon' => '#E9967A', + 'darkseagreen' => '#8FBC8F', + 'darkslateblue' => '#483D8B', + 'darkslategray' => '#2F4F4F', + 'darkslategrey' => '#2F4F4F', + 'darkturquoise' => '#00CED1', + 'darkviolet' => '#9400D3', + 'deeppink' => '#FF1493', + 'deepskyblue' => '#00BFFF', + 'dimgray' => '#696969', + 'dimgrey' => '#696969', + 'dodgerblue' => '#1E90FF', + 'firebrick' => '#B22222', + 'floralwhite' => '#FFFAF0', + 'forestgreen' => '#228B22', + 'fuchsia' => '#FF00FF', + 'gainsboro' => '#DCDCDC', + 'ghostwhite' => '#F8F8FF', + 'gold' => '#FFD700', + 'goldenrod' => '#DAA520', + 'gray' => '#808080', + 'grey' => '#808080', + 'green' => '#008000', + 'greenyellow' => '#ADFF2F', + 'honeydew' => '#F0FFF0', + 'hotpink' => '#FF69B4', + 'indianred' => '#CD5C5C', + 'indigo' => '#4B0082', + 'ivory' => '#FFFFF0', + 'khaki' => '#F0E68C', + 'lavender' => '#E6E6FA', + 'lavenderblush' => '#FFF0F5', + 'lawngreen' => '#7CFC00', + 'lemonchiffon' => '#FFFACD', + 'lightblue' => '#ADD8E6', + 'lightcoral' => '#F08080', + 'lightcyan' => '#E0FFFF', + 'lightgoldenrodyellow' => '#FAFAD2', + 'lightgray' => '#D3D3D3', + 'lightgrey' => '#D3D3D3', + 'lightgreen' => '#90EE90', + 'lightpink' => '#FFB6C1', + 'lightsalmon' => '#FFA07A', + 'lightseagreen' => '#20B2AA', + 'lightskyblue' => '#87CEFA', + 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#B0C4DE', + 'lightyellow' => '#FFFFE0', + 'lime' => '#00FF00', + 'limegreen' => '#32CD32', + 'linen' => '#FAF0E6', + 'magenta' => '#FF00FF', + 'maroon' => '#800000', + 'mediumaquamarine' => '#66CDAA', + 'mediumblue' => '#0000CD', + 'mediumorchid' => '#BA55D3', + 'mediumpurple' => '#9370DB', + 'mediumseagreen' => '#3CB371', + 'mediumslateblue' => '#7B68EE', + 'mediumspringgreen' => '#00FA9A', + 'mediumturquoise' => '#48D1CC', + 'mediumvioletred' => '#C71585', + 'midnightblue' => '#191970', + 'mintcream' => '#F5FFFA', + 'mistyrose' => '#FFE4E1', + 'moccasin' => '#FFE4B5', + 'navajowhite' => '#FFDEAD', + 'navy' => '#000080', + 'oldlace' => '#FDF5E6', + 'olive' => '#808000', + 'olivedrab' => '#6B8E23', + 'orange' => '#FFA500', + 'orangered' => '#FF4500', + 'orchid' => '#DA70D6', + 'palegoldenrod' => '#EEE8AA', + 'palegreen' => '#98FB98', + 'paleturquoise' => '#AFEEEE', + 'palevioletred' => '#DB7093', + 'papayawhip' => '#FFEFD5', + 'peachpuff' => '#FFDAB9', + 'peru' => '#CD853F', + 'pink' => '#FFC0CB', + 'plum' => '#DDA0DD', + 'powderblue' => '#B0E0E6', + 'purple' => '#800080', + 'rebeccapurple' => '#663399', + 'red' => '#FF0000', + 'rosybrown' => '#BC8F8F', + 'royalblue' => '#4169E1', + 'saddlebrown' => '#8B4513', + 'salmon' => '#FA8072', + 'sandybrown' => '#F4A460', + 'seagreen' => '#2E8B57', + 'seashell' => '#FFF5EE', + 'sienna' => '#A0522D', + 'silver' => '#C0C0C0', + 'skyblue' => '#87CEEB', + 'slateblue' => '#6A5ACD', + 'slategray' => '#708090', + 'slategrey' => '#708090', + 'snow' => '#FFFAFA', + 'springgreen' => '#00FF7F', + 'steelblue' => '#4682B4', + 'tan' => '#D2B48C', + 'teal' => '#008080', + 'thistle' => '#D8BFD8', + 'tomato' => '#FF6347', + 'turquoise' => '#40E0D0', + 'violet' => '#EE82EE', + 'wheat' => '#F5DEB3', + 'white' => '#FFFFFF', + 'whitesmoke' => '#F5F5F5', + 'yellow' => '#FFFF00', + 'yellowgreen' => '#9ACD32' +) +--DESCRIPTION-- + +Lookup array of color names to six digit hexadecimal number corresponding +to color, with preceding hash mark. Used when parsing colors. The lookup +is done in a case-insensitive manner. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt new file mode 100644 index 0000000..60cb409 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt @@ -0,0 +1,15 @@ +Core.ConvertDocumentToFragment +TYPE: bool +DEFAULT: true +--DESCRIPTION-- + +This parameter determines whether or not the filter should convert +input that is a full document with html and body tags to a fragment +of just the contents of a body tag. This parameter is simply something +HTML Purifier can do during an edge-case: for most inputs, this +processing is not necessary. Warning: Full HTML purification has not +been implemented. See GitHub issue #7. + +--ALIASES-- +Core.AcceptFullDocuments +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt new file mode 100644 index 0000000..36f16e0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt @@ -0,0 +1,17 @@ +Core.DirectLexLineNumberSyncInterval +TYPE: int +VERSION: 2.0.0 +DEFAULT: 0 +--DESCRIPTION-- + +

+ Specifies the number of tokens the DirectLex line number tracking + implementations should process before attempting to resyncronize the + current line count by manually counting all previous new-lines. When + at 0, this functionality is disabled. Lower values will decrease + performance, and this is only strictly necessary if the counting + algorithm is buggy (in which case you should report it as a bug). + This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is + not being used. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt new file mode 100644 index 0000000..1cd4c2c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt @@ -0,0 +1,14 @@ +Core.DisableExcludes +TYPE: bool +DEFAULT: false +VERSION: 4.5.0 +--DESCRIPTION-- +

+ This directive disables SGML-style exclusions, e.g. the exclusion of + <object> in any descendant of a + <pre> tag. Disabling excludes will allow some + invalid documents to pass through HTML Purifier, but HTML Purifier + will also be less likely to accidentally remove large documents during + processing. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt new file mode 100644 index 0000000..ce243c3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt @@ -0,0 +1,9 @@ +Core.EnableIDNA +TYPE: bool +DEFAULT: false +VERSION: 4.4.0 +--DESCRIPTION-- +Allows international domain names in URLs. This configuration option +requires the PEAR Net_IDNA2 module to be installed. It operates by +punycoding any internationalized host names for maximum portability. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt new file mode 100644 index 0000000..8bfb47c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt @@ -0,0 +1,15 @@ +Core.Encoding +TYPE: istring +DEFAULT: 'utf-8' +--DESCRIPTION-- +If for some reason you are unable to convert all webpages to UTF-8, you can +use this directive as a stop-gap compatibility change to let HTML Purifier +deal with non UTF-8 input. This technique has notable deficiencies: +absolutely no characters outside of the selected character encoding will be +preserved, not even the ones that have been ampersand escaped (this is due +to a UTF-8 specific feature that automatically resolves all +entities), making it pretty useless for anything except the most I18N-blind +applications, although %Core.EscapeNonASCIICharacters offers fixes this +trouble with another tradeoff. This directive only accepts ISO-8859-1 if +iconv is not enabled. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt new file mode 100644 index 0000000..a3881be --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt @@ -0,0 +1,12 @@ +Core.EscapeInvalidChildren +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +

Warning: this configuration option is no longer does anything as of 4.6.0.

+ +

When true, a child is found that is not allowed in the context of the +parent element will be transformed into text as if it were ASCII. When +false, that element and all internal tags will be dropped, though text will +be preserved. There is no option for dropping the element but preserving +child nodes.

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt new file mode 100644 index 0000000..a7a5b24 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt @@ -0,0 +1,7 @@ +Core.EscapeInvalidTags +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When true, invalid tags will be written back to the document as plain text. +Otherwise, they are silently dropped. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt new file mode 100644 index 0000000..4eedb34 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt @@ -0,0 +1,13 @@ +Core.EscapeNonASCIICharacters +TYPE: bool +VERSION: 1.4.0 +DEFAULT: false +--DESCRIPTION-- +This directive overcomes a deficiency in %Core.Encoding by blindly +converting all non-ASCII characters into decimal numeric entities before +converting it to its native encoding. This means that even characters that +can be expressed in the non-UTF-8 encoding will be entity-ized, which can +be a real downer for encodings like Big5. It also assumes that the ASCII +repertoire is available, although this is the case for almost all encodings. +Anyway, use UTF-8! +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt new file mode 100644 index 0000000..915391e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt @@ -0,0 +1,19 @@ +Core.HiddenElements +TYPE: lookup +--DEFAULT-- +array ( + 'script' => true, + 'style' => true, +) +--DESCRIPTION-- + +

+ This directive is a lookup array of elements which should have their + contents removed when they are not allowed by the HTML definition. + For example, the contents of a script tag are not + normally shown in a document, so if script tags are to be removed, + their contents should be removed to. This is opposed to a b + tag, which defines some presentational changes but does not hide its + contents. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt new file mode 100644 index 0000000..233fca1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt @@ -0,0 +1,10 @@ +Core.Language +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'en' +--DESCRIPTION-- + +ISO 639 language code for localizable things in HTML Purifier to use, +which is mainly error reporting. There is currently only an English (en) +translation, so this directive is currently useless. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt new file mode 100644 index 0000000..392b436 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt @@ -0,0 +1,36 @@ +Core.LegacyEntityDecoder +TYPE: bool +VERSION: 4.9.0 +DEFAULT: false +--DESCRIPTION-- +

+ Prior to HTML Purifier 4.9.0, entities were decoded by performing + a global search replace for all entities whose decoded versions + did not have special meanings under HTML, and replaced them with + their decoded versions. We would match all entities, even if they did + not have a trailing semicolon, but only if there weren't any trailing + alphanumeric characters. +

+ + + + + + +
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena&yena&yena
&yen=¥=¥=
+

+ In HTML Purifier 4.9.0, we changed the behavior of entity parsing + to match entities that had missing trailing semicolons in less + cases, to more closely match HTML5 parsing behavior: +

+ + + + + + +
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena¥a&yena
&yen=¥=&yen=
+

+ This flag reverts back to pre-HTML Purifier 4.9.0 behavior. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt new file mode 100644 index 0000000..e469b88 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -0,0 +1,34 @@ +Core.LexerImpl +TYPE: mixed/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ This parameter determines what lexer implementation can be used. The + valid values are: +

+
+
null
+
+ Recommended, the lexer implementation will be auto-detected based on + your PHP-version and configuration. +
+
string lexer identifier
+
+ This is a slim way of manually overriding the implementation. + Currently recognized values are: DOMLex (the default PHP5 +implementation) + and DirectLex (the default PHP4 implementation). Only use this if + you know what you are doing: usually, the auto-detection will + manage things for cases you aren't even aware of. +
+
object lexer instance
+
+ Super-advanced: you can specify your own, custom, implementation that + implements the interface defined by HTMLPurifier_Lexer. + I may remove this option simply because I don't expect anyone + to use it. +
+
+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt new file mode 100644 index 0000000..eb841a7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt @@ -0,0 +1,16 @@ +Core.MaintainLineNumbers +TYPE: bool/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If true, HTML Purifier will add line number information to all tokens. + This is useful when error reporting is turned on, but can result in + significant performance degradation and should not be used when + unnecessary. This directive must be used with the DirectLex lexer, + as the DOMLex lexer does not (yet) support this functionality. + If the value is null, an appropriate value will be selected based + on other configuration. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt new file mode 100644 index 0000000..d77f536 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt @@ -0,0 +1,11 @@ +Core.NormalizeNewlines +TYPE: bool +VERSION: 4.2.0 +DEFAULT: true +--DESCRIPTION-- +

+ Whether or not to normalize newlines to the operating + system default. When false, HTML Purifier + will attempt to preserve mixed newline files. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveBlanks.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveBlanks.txt new file mode 100644 index 0000000..95e5285 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveBlanks.txt @@ -0,0 +1,10 @@ +Core.RemoveBlanks +TYPE: bool +DEFAULT: false +VERSION: 4.18 +--DESCRIPTION-- +

+ If set to true, blank nodes will be removed. This can be useful for maintaining + backwards compatibility when upgrading from previous versions of PHP. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt new file mode 100644 index 0000000..4070c2a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt @@ -0,0 +1,12 @@ +Core.RemoveInvalidImg +TYPE: bool +DEFAULT: true +VERSION: 1.3.0 +--DESCRIPTION-- + +

+ This directive enables pre-emptive URI checking in img + tags, as the attribute validation strategy is not authorized to + remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt new file mode 100644 index 0000000..3397d9f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt @@ -0,0 +1,11 @@ +Core.RemoveProcessingInstructions +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +Instead of escaping processing instructions in the form <? ... +?>, remove it out-right. This may be useful if the HTML +you are validating contains XML processing instruction gunk, however, +it can also be user-unfriendly for people attempting to post PHP +snippets. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt new file mode 100644 index 0000000..a4cd966 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt @@ -0,0 +1,12 @@ +Core.RemoveScriptContents +TYPE: bool/null +DEFAULT: NULL +VERSION: 2.0.0 +DEPRECATED-VERSION: 2.1.0 +DEPRECATED-USE: Core.HiddenElements +--DESCRIPTION-- +

+ This directive enables HTML Purifier to remove not only script tags + but all of their contents. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt new file mode 100644 index 0000000..3db50ef --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt @@ -0,0 +1,11 @@ +Filter.Custom +TYPE: list +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This directive can be used to add custom filters; it is nearly the + equivalent of the now deprecated HTMLPurifier->addFilter() + method. Specify an array of concrete implementations. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt new file mode 100644 index 0000000..16829bc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt @@ -0,0 +1,14 @@ +Filter.ExtractStyleBlocks.Escaping +TYPE: bool +VERSION: 3.0.0 +DEFAULT: true +ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping +--DESCRIPTION-- + +

+ Whether or not to escape the dangerous characters <, > and & + as \3C, \3E and \26, respectively. This is can be safely set to false + if the contents of StyleBlocks will be placed in an external stylesheet, + where there is no risk of it being interpreted as HTML. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt new file mode 100644 index 0000000..7f95f54 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt @@ -0,0 +1,29 @@ +Filter.ExtractStyleBlocks.Scope +TYPE: string/null +VERSION: 3.0.0 +DEFAULT: NULL +ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope +--DESCRIPTION-- + +

+ If you would like users to be able to define external stylesheets, but + only allow them to specify CSS declarations for a specific node and + prevent them from fiddling with other elements, use this directive. + It accepts any valid CSS selector, and will prepend this to any + CSS declaration extracted from the document. For example, if this + directive is set to #user-content and a user uses the + selector a:hover, the final selector will be + #user-content a:hover. +

+

+ The comma shorthand may be used; consider the above example, with + #user-content, #user-content2, the final selector will + be #user-content a:hover, #user-content2 a:hover. +

+

+ Warning: It is possible for users to bypass this measure + using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML + Purifier, and I am working to get it fixed. Until then, HTML Purifier + performs a basic check to prevent this. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt new file mode 100644 index 0000000..6c231b2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt @@ -0,0 +1,16 @@ +Filter.ExtractStyleBlocks.TidyImpl +TYPE: mixed/null +VERSION: 3.1.0 +DEFAULT: NULL +ALIASES: FilterParam.ExtractStyleBlocksTidyImpl +--DESCRIPTION-- +

+ If left NULL, HTML Purifier will attempt to instantiate a csstidy + class to use for internal cleaning. This will usually be good enough. +

+

+ However, for trusted user input, you can set this to false to + disable cleaning. In addition, you can supply your own concrete implementation + of Tidy's interface to use, although I don't know why you'd want to do that. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt new file mode 100644 index 0000000..078d087 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt @@ -0,0 +1,74 @@ +Filter.ExtractStyleBlocks +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +EXTERNAL: CSSTidy +--DESCRIPTION-- +

+ This directive turns on the style block extraction filter, which removes + style blocks from input HTML, cleans them up with CSSTidy, + and places them in the StyleBlocks context variable, for further + use by you, usually to be placed in an external stylesheet, or a + style block in the head of your document. +

+

+ Sample usage: +

+
';
+?>
+
+
+
+  Filter.ExtractStyleBlocks
+body {color:#F00;} Some text';
+
+    $config = HTMLPurifier_Config::createDefault();
+    $config->set('Filter', 'ExtractStyleBlocks', true);
+    $purifier = new HTMLPurifier($config);
+
+    $html = $purifier->purify($dirty);
+
+    // This implementation writes the stylesheets to the styles/ directory.
+    // You can also echo the styles inside the document, but it's a bit
+    // more difficult to make sure they get interpreted properly by
+    // browsers; try the usual CSS armoring techniques.
+    $styles = $purifier->context->get('StyleBlocks');
+    $dir = 'styles/';
+    if (!is_dir($dir)) mkdir($dir);
+    $hash = sha1($_GET['html']);
+    foreach ($styles as $i => $style) {
+        file_put_contents($name = $dir . $hash . "_$i");
+        echo '';
+    }
+?>
+
+
+  
+ +
+ + +]]>
+

+ Warning: It is possible for a user to mount an + imagecrash attack using this CSS. Counter-measures are difficult; + it is not simply enough to limit the range of CSS lengths (using + relative lengths with many nesting levels allows for large values + to be attained without actually specifying them in the stylesheet), + and the flexible nature of selectors makes it difficult to selectively + disable lengths on image tags (HTML Purifier, however, does disable + CSS width and height in inline styling). There are probably two effective + counter measures: an explicit width and height set to auto in all + images in your document (unlikely) or the disabling of width and + height (somewhat reasonable). Whether or not these measures should be + used is left to the reader. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt new file mode 100644 index 0000000..321eaa2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -0,0 +1,16 @@ +Filter.YouTube +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ Warning: Deprecated in favor of %HTML.SafeObject and + %Output.FlashCompat (turn both on to allow YouTube videos and other + Flash content). +

+

+ This directive enables YouTube video embedding in HTML Purifier. Check + this document + on embedding videos for more information on what this filter does. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt new file mode 100644 index 0000000..0b2c106 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -0,0 +1,25 @@ +HTML.Allowed +TYPE: itext/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ This is a preferred convenience directive that combines + %HTML.AllowedElements and %HTML.AllowedAttributes. + Specify elements and attributes that are allowed using: + element1[attr1|attr2],element2.... For example, + if you would like to only allow paragraphs and links, specify + a[href],p. You can specify attributes that apply + to all elements using an asterisk, e.g. *[lang]. + You can also use newlines instead of commas to separate elements. +

+

+ Warning: + All of the constraints on the component directives are still enforced. + The syntax is a subset of TinyMCE's valid_elements + whitelist: directly copy-pasting it here will probably result in + broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes + are set, this directive has no effect. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt new file mode 100644 index 0000000..fcf093f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -0,0 +1,19 @@ +HTML.AllowedAttributes +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If HTML Purifier's attribute set is unsatisfactory, overload it! + The syntax is "tag.attr" or "*.attr" for the global attributes + (style, id, class, dir, lang, xml:lang). +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. For + example, %HTML.EnableAttrID will take precedence over *.id in this + directive. You must set that directive to true before you can use + IDs at all. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt new file mode 100644 index 0000000..140e214 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt @@ -0,0 +1,10 @@ +HTML.AllowedComments +TYPE: lookup +VERSION: 4.4.0 +DEFAULT: array() +--DESCRIPTION-- +A whitelist which indicates what explicit comment bodies should be +allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp +(these directives are union'ed together, so a comment is considered +valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt new file mode 100644 index 0000000..f22e977 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt @@ -0,0 +1,15 @@ +HTML.AllowedCommentsRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +A regexp, which if it matches the body of a comment, indicates that +it should be allowed. Trailing and leading spaces are removed prior +to running this regular expression. +Warning: Make sure you specify +correct anchor metacharacters ^regex$, otherwise you may accept +comments that you did not mean to! In particular, the regex /foo|bar/ +is probably not sufficiently strict, since it also allows foobar. +See also %HTML.AllowedComments (these directives are union'ed together, +so a comment is considered valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt new file mode 100644 index 0000000..1d3fa79 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -0,0 +1,23 @@ +HTML.AllowedElements +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ If HTML Purifier's tag set is unsatisfactory for your needs, you can + overload it with your own list of tags to allow. If you change + this, you probably also want to change %HTML.AllowedAttributes; see + also %HTML.Allowed which lets you set allowed elements and + attributes at the same time. +

+

+ If you attempt to allow an element that HTML Purifier does not know + about, HTML Purifier will raise an error. You will need to manually + tell HTML Purifier about this element by using the + advanced customization features. +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt new file mode 100644 index 0000000..5a59a55 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -0,0 +1,20 @@ +HTML.AllowedModules +TYPE: lookup/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ A doctype comes with a set of usual modules to use. Without having + to mucking about with the doctypes, you can quickly activate or + disable these modules by specifying which modules you wish to allow + with this directive. This is most useful for unit testing specific + modules, although end users may find it useful for their own ends. +

+

+ If you specify a module that does not exist, the manager will silently + fail to use it, so be careful! User-defined modules are not affected + by this directive. Modules defined in %HTML.CoreModules are not + affected by this directive. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt new file mode 100644 index 0000000..151fb7b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt @@ -0,0 +1,11 @@ +HTML.Attr.Name.UseCDATA +TYPE: bool +DEFAULT: false +VERSION: 4.0.0 +--DESCRIPTION-- +The W3C specification DTD defines the name attribute to be CDATA, not ID, due +to limitations of DTD. In certain documents, this relaxed behavior is desired, +whether it is to specify duplicate names, or to specify names that would be +illegal IDs (for example, names that begin with a digit.) Set this configuration +directive to true to use the relaxed parsing rules. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt new file mode 100644 index 0000000..45ae469 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -0,0 +1,18 @@ +HTML.BlockWrapper +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'p' +--DESCRIPTION-- + +

+ String name of element to wrap inline elements that are inside a block + context. This only occurs in the children of blockquote in strict mode. +

+

+ Example: by default value, + <blockquote>Foo</blockquote> would become + <blockquote><p>Foo</p></blockquote>. + The <p> tags can be replaced with whatever you desire, + as long as it is a block level element. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt new file mode 100644 index 0000000..5246188 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt @@ -0,0 +1,23 @@ +HTML.CoreModules +TYPE: lookup +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'Structure' => true, + 'Text' => true, + 'Hypertext' => true, + 'List' => true, + 'NonXMLCommonAttributes' => true, + 'XMLCommonAttributes' => true, + 'CommonAttributes' => true, +) +--DESCRIPTION-- + +

+ Certain modularized doctypes (XHTML, namely), have certain modules + that must be included for the doctype to be an conforming document + type: put those modules here. By default, XHTML's core modules + are used. You can set this to a blank array to disable core module + protection, but this is not recommended. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt new file mode 100644 index 0000000..6ed70b5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt @@ -0,0 +1,9 @@ +HTML.CustomDoctype +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +A custom doctype for power-users who defined their own document +type. This directive only applies when %HTML.Doctype is blank. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt new file mode 100644 index 0000000..103db75 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -0,0 +1,33 @@ +HTML.DefinitionID +TYPE: string/null +DEFAULT: NULL +VERSION: 2.0.0 +--DESCRIPTION-- + +

+ Unique identifier for a custom-built HTML definition. If you edit + the raw version of the HTMLDefinition, introducing changes that the + configuration object does not reflect, you must specify this variable. + If you change your custom edits, you should change this directive, or + clear your cache. Example: +

+
+$config = HTMLPurifier_Config::createDefault();
+$config->set('HTML', 'DefinitionID', '1');
+$def = $config->getHTMLDefinition();
+$def->addAttribute('a', 'tabindex', 'Number');
+
+

+ In the above example, the configuration is still at the defaults, but + using the advanced API, an extra attribute has been added. The + configuration object normally has no way of knowing that this change + has taken place, so it needs an extra directive: %HTML.DefinitionID. + If someone else attempts to use the default configuration, these two + pieces of code will not clobber each other in the cache, since one has + an extra directive attached to it. +

+

+ You must specify a value to this directive to use the + advanced API features. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt new file mode 100644 index 0000000..229ae02 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -0,0 +1,16 @@ +HTML.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition specified in + %HTML.DefinitionID. This serves the same purpose: uniquely identifying + your custom definition, but this one does so in a chronological + context: revision 3 is more up-to-date then revision 2. Thus, when + this gets incremented, the cache handling is smart enough to clean + up any older revisions of your definition as well as flush the + cache. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt new file mode 100644 index 0000000..9dab497 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt @@ -0,0 +1,11 @@ +HTML.Doctype +TYPE: string/null +DEFAULT: NULL +--DESCRIPTION-- +Doctype to use during filtering. Technically speaking this is not actually +a doctype (as it does not identify a corresponding DTD), but we are using +this name for sake of simplicity. When non-blank, this will override any +older directives like %HTML.XHTML or %HTML.Strict. +--ALLOWED-- +'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt new file mode 100644 index 0000000..7878dc0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt @@ -0,0 +1,11 @@ +HTML.FlashAllowFullScreen +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit embedded Flash content from + %HTML.SafeObject to expand to the full screen. Corresponds to + the allowFullScreen parameter. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt new file mode 100644 index 0000000..57358f9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -0,0 +1,21 @@ +HTML.ForbiddenAttributes +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ While this directive is similar to %HTML.AllowedAttributes, for + forwards-compatibility with XML, this attribute has a different syntax. Instead of + tag.attr, use tag@attr. To disallow href + attributes in a tags, set this directive to + a@href. You can also disallow an attribute globally with + attr or *@attr (either syntax is fine; the latter + is provided for consistency with %HTML.AllowedAttributes). +

+

+ Warning: This directive complements %HTML.ForbiddenElements, + accordingly, check + out that directive for a discussion of why you + should think twice before using this directive. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt new file mode 100644 index 0000000..93a53e1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -0,0 +1,20 @@ +HTML.ForbiddenElements +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This was, perhaps, the most requested feature ever in HTML + Purifier. Please don't abuse it! This is the logical inverse of + %HTML.AllowedElements, and it will override that directive, or any + other directive. +

+

+ If possible, %HTML.Allowed is recommended over this directive, because it + can sometimes be difficult to tell whether or not you've forbidden all of + the behavior you would like to disallow. If you forbid img + with the expectation of preventing images on your site, you'll be in for + a nasty surprise when people start using the background-image + CSS property. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt new file mode 100644 index 0000000..4a432d8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt @@ -0,0 +1,11 @@ +HTML.Forms +TYPE: bool +VERSION: 4.13.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit form elements in the user input, regardless of + %HTML.Trusted value. Please be very careful when using this functionality, as + enabling forms in untrusted documents may allow for phishing attacks. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt new file mode 100644 index 0000000..b2591e4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -0,0 +1,14 @@ +HTML.MaxImgLength +TYPE: int/null +DEFAULT: null +VERSION: 3.1.1 +--DESCRIPTION-- +

+ This directive controls the maximum number of pixels in the width and + height attributes in img tags. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %CSS.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the HTML max is an integer). +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt new file mode 100644 index 0000000..700b309 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt @@ -0,0 +1,7 @@ +HTML.Nofollow +TYPE: bool +VERSION: 4.3.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, nofollow rel attributes are added to all outgoing links. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt new file mode 100644 index 0000000..62e8e16 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -0,0 +1,12 @@ +HTML.Parent +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'div' +--DESCRIPTION-- + +

+ String name of element that HTML fragment passed to library will be + inserted in. An interesting variation would be using span as the + parent element, meaning that only inline tags would be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt new file mode 100644 index 0000000..dfb7204 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -0,0 +1,12 @@ +HTML.Proprietary +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to allow proprietary elements and attributes in your + documents, as per HTMLPurifier_HTMLModule_Proprietary. + Warning: This can cause your documents to stop + validating! +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt new file mode 100644 index 0000000..cdda09a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -0,0 +1,13 @@ +HTML.SafeEmbed +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit embed tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to embed tags. Embed is a proprietary + element and will cause your website to stop validating; you should + see if you can use %Output.FlashCompat with %HTML.SafeObject instead + first.

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt new file mode 100644 index 0000000..5a5c103 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -0,0 +1,13 @@ +HTML.SafeIframe +TYPE: bool +VERSION: 4.4.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit iframe tags in untrusted documents. This + directive must be accompanied by a whitelist of permitted iframes, + such as %URI.SafeIframeRegexp or %URI.SafeIframeHosts, otherwise it will fatally error. + This directive has no effect on strict doctypes, as iframes are not + valid. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt new file mode 100644 index 0000000..ceb342e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -0,0 +1,13 @@ +HTML.SafeObject +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit object tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to object tags. You should also enable + %Output.FlashCompat in order to generate Internet Explorer + compatibility code for your object tags. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt new file mode 100644 index 0000000..5ebc7a1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt @@ -0,0 +1,10 @@ +HTML.SafeScripting +TYPE: lookup +VERSION: 4.5.0 +DEFAULT: array() +--DESCRIPTION-- +

+ Whether or not to permit script tags to external scripts in documents. + Inline scripting is not allowed, and the script must match an explicit whitelist. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt new file mode 100644 index 0000000..a8b1de5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt @@ -0,0 +1,9 @@ +HTML.Strict +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not to use Transitional (loose) or Strict rulesets. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt new file mode 100644 index 0000000..587a167 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt @@ -0,0 +1,8 @@ +HTML.TargetBlank +TYPE: bool +VERSION: 4.4.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, target=blank attributes are added to all outgoing links. +(This includes links from an HTTPS version of a page to an HTTP version.) +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt new file mode 100644 index 0000000..dd514c0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt @@ -0,0 +1,10 @@ +--# vim: et sw=4 sts=4 +HTML.TargetNoopener +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noopener rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt new file mode 100644 index 0000000..cb5a0b0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt @@ -0,0 +1,9 @@ +HTML.TargetNoreferrer +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noreferrer rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt new file mode 100644 index 0000000..b4c271b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt @@ -0,0 +1,8 @@ +HTML.TidyAdd +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to add to the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt new file mode 100644 index 0000000..4186ccd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -0,0 +1,24 @@ +HTML.TidyLevel +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'medium' +--DESCRIPTION-- + +

General level of cleanliness the Tidy module should enforce. +There are four allowed values:

+
+
none
+
No extra tidying should be done
+
light
+
Only fix elements that would be discarded otherwise due to + lack of support in doctype
+
medium
+
Enforce best practices
+
heavy
+
Transform all deprecated elements and attributes to standards + compliant equivalents
+
+ +--ALLOWED-- +'none', 'light', 'medium', 'heavy' +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt new file mode 100644 index 0000000..996762b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt @@ -0,0 +1,8 @@ +HTML.TidyRemove +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to remove from the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt new file mode 100644 index 0000000..1db9237 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -0,0 +1,9 @@ +HTML.Trusted +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user input is trusted or not. If the input is +trusted, a more expansive set of allowed tags and attributes will be used. +See also %CSS.Trusted. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt new file mode 100644 index 0000000..2a47e38 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt @@ -0,0 +1,11 @@ +HTML.XHTML +TYPE: bool +DEFAULT: true +VERSION: 1.1.0 +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. +--ALIASES-- +Core.XHTML +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt new file mode 100644 index 0000000..08921fd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt @@ -0,0 +1,10 @@ +Output.CommentScriptContents +TYPE: bool +VERSION: 2.0.0 +DEFAULT: true +--DESCRIPTION-- +Determines whether or not HTML Purifier should attempt to fix up the +contents of script tags for legacy browsers with comments. +--ALIASES-- +Core.CommentScriptContents +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt new file mode 100644 index 0000000..d6f0d9f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt @@ -0,0 +1,15 @@ +Output.FixInnerHTML +TYPE: bool +VERSION: 4.3.0 +DEFAULT: true +--DESCRIPTION-- +

+ If true, HTML Purifier will protect against Internet Explorer's + mishandling of the innerHTML attribute by appending + a space to any attribute that does not contain angled brackets, spaces + or quotes, but contains a backtick. This slightly changes the + semantics of any given attribute, so if this is unacceptable and + you do not use innerHTML on any of your pages, you can + turn this directive off. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt new file mode 100644 index 0000000..93398e8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -0,0 +1,11 @@ +Output.FlashCompat +TYPE: bool +VERSION: 4.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ If true, HTML Purifier will generate Internet Explorer compatibility + code for all object code. This is highly recommended if you enable + %HTML.SafeObject. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt new file mode 100644 index 0000000..79f8ad8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -0,0 +1,13 @@ +Output.Newline +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Newline string to format final output with. If left null, HTML Purifier + will auto-detect the default newline type of the system and use that; + you can manually override it here. Remember, \r\n is Windows, \r + is Mac, and \n is Unix. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt new file mode 100644 index 0000000..232b023 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -0,0 +1,14 @@ +Output.SortAttr +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ If true, HTML Purifier will sort attributes by name before writing them back + to the document, converting a tag like: <el b="" a="" c="" /> + to <el a="" b="" c="" />. This is a workaround for + a bug in FCKeditor which causes it to swap attributes order, adding noise + to text diffs. If you're not seeing this bug, chances are, you don't need + this directive. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt new file mode 100644 index 0000000..06bab00 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -0,0 +1,25 @@ +Output.TidyFormat +TYPE: bool +VERSION: 1.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +

+

+ This can greatly improve readability for editors who are hand-editing + the HTML, but is by no means necessary as HTML Purifier has already + fixed all major errors the HTML may have had. Tidy is a non-default + extension, and this directive will silently fail if Tidy is not + available. +

+

+ If you are looking to make the overall look of your page's source + better, I recommend running Tidy on the entire page rather than just + user-content (after all, the indentation relative to the containing + blocks will be incorrect). +

+--ALIASES-- +Core.TidyFormat +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt new file mode 100644 index 0000000..071bc02 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt @@ -0,0 +1,7 @@ +Test.ForceNoIconv +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When set to true, HTMLPurifier_Encoder will act as if iconv does not exist +and use only pure PHP implementations. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt new file mode 100644 index 0000000..eb97307 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -0,0 +1,18 @@ +URI.AllowedSchemes +TYPE: lookup +--DEFAULT-- +array ( + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, + 'tel' => true, +) +--DESCRIPTION-- +Whitelist that defines the schemes that a URI is allowed to have. This +prevents XSS attacks from using pseudo-schemes like javascript or mocha. +There is also support for the data and file +URI schemes, but they are not enabled by default. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSymbols.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSymbols.txt new file mode 100644 index 0000000..d89a5f6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSymbols.txt @@ -0,0 +1,7 @@ +URI.AllowedSymbols +TYPE: string/null +DEFAULT: '!$&\'()*+,;=' +--DESCRIPTION-- +If a system permits templated URLs, then the URI encoder may need extra +hints about which symbols to preserve. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt new file mode 100644 index 0000000..876f068 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -0,0 +1,17 @@ +URI.Base +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ The base URI is the URI of the document this purified HTML will be + inserted into. This information is important if HTML Purifier needs + to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute + is on. You may use a non-absolute URI for this value, but behavior + may vary (%URI.MakeAbsolute deals nicely with both absolute and + relative paths, but forwards-compatibility is not guaranteed). + Warning: If set, the scheme on this URI + overrides the one specified by %URI.DefaultScheme. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt new file mode 100644 index 0000000..834bc08 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -0,0 +1,15 @@ +URI.DefaultScheme +TYPE: string/null +DEFAULT: 'http' +--DESCRIPTION-- + +

+ Defines through what scheme the output will be served, in order to + select the proper object validator when no scheme information is present. +

+ +

+ Starting with HTML Purifier 4.9.0, the default scheme can be null, in + which case we reject all URIs which do not have explicit schemes. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt new file mode 100644 index 0000000..f05312b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -0,0 +1,11 @@ +URI.DefinitionID +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Unique identifier for a custom-built URI definition. If you want + to add custom URIFilters, you must specify this value. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt new file mode 100644 index 0000000..80cfea9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -0,0 +1,11 @@ +URI.DefinitionRev +TYPE: int +VERSION: 2.1.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt new file mode 100644 index 0000000..71ce025 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -0,0 +1,14 @@ +URI.Disable +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Disables all URIs in all forms. Not sure why you'd want to do that + (after all, the Internet's founded on the notion of a hyperlink). +

+ +--ALIASES-- +Attr.DisableURI +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt new file mode 100644 index 0000000..13c122c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt @@ -0,0 +1,11 @@ +URI.DisableExternal +TYPE: bool +VERSION: 1.2.0 +DEFAULT: false +--DESCRIPTION-- +Disables links to external websites. This is a highly effective anti-spam +and anti-pagerank-leech measure, but comes at a hefty price: nolinks or +images outside of your domain will be allowed. Non-linkified URIs will +still be preserved. If you want to be able to link to subdomains or use +absolute URIs, specify %URI.Host for your website. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt new file mode 100644 index 0000000..abcc1ef --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt @@ -0,0 +1,13 @@ +URI.DisableExternalResources +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- +Disables the embedding of external resources, preventing users from +embedding things like images from other hosts. This prevents access +tracking (good for email viewers), bandwidth leeching, cross-site request +forging, goatse.cx posting, and other nasties, but also results in a loss +of end-user functionality (they can't directly post a pic they posted from +Flickr anymore). Use it if you don't have a robust user-content moderation +team. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt new file mode 100644 index 0000000..f891de4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -0,0 +1,15 @@ +URI.DisableResources +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ Disables embedding resources, essentially meaning no pictures. You can + still link to them though. See %URI.DisableExternalResources for why + this might be a good idea. +

+

+ Note: While this directive has been available since 1.3.0, + it didn't actually start doing anything until 4.2.0. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt new file mode 100644 index 0000000..ee83b12 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -0,0 +1,19 @@ +URI.Host +TYPE: string/null +VERSION: 1.2.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Defines the domain name of the server, so we can determine whether or + an absolute URI is from your website or not. Not strictly necessary, + as users should be using relative URIs to reference resources on your + website. It will, however, let you use absolute URIs to link to + subdomains of the domain you post here: i.e. example.com will allow + sub.example.com. However, higher up domains will still be excluded: + if you set %URI.Host to sub.example.com, example.com will be blocked. + Note: This directive overrides %URI.Base because + a given page may be on a sub-domain, but you wish HTML Purifier to be + more relaxed and allow some of the parent domains too. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt new file mode 100644 index 0000000..0b6df76 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt @@ -0,0 +1,9 @@ +URI.HostBlacklist +TYPE: list +VERSION: 1.3.0 +DEFAULT: array() +--DESCRIPTION-- +List of strings that are forbidden in the host of any URI. Use it to kill +domain names of spam, etc. Note that it will catch anything in the domain, +so moo.com will catch moo.com.example.com. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt new file mode 100644 index 0000000..4214900 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -0,0 +1,13 @@ +URI.MakeAbsolute +TYPE: bool +VERSION: 2.1.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Converts all URIs into absolute forms. This is useful when the HTML + being filtered assumes a specific base path, but will actually be + viewed in a different context (and setting an alternate base URI is + not possible). %URI.Base must be set for this directive to work. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt new file mode 100644 index 0000000..58c81dc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -0,0 +1,83 @@ +URI.Munge +TYPE: string/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Munges all browsable (usually http, https and ftp) + absolute URIs into another URI, usually a URI redirection service. + This directive accepts a URI, formatted with a %s where + the url-encoded original URI should be inserted (sample: + http://www.google.com/url?q=%s). +

+

+ Uses for this directive: +

+
    +
  • + Prevent PageRank leaks, while being fairly transparent + to users (you may also want to add some client side JavaScript to + override the text in the statusbar). Notice: + Many security experts believe that this form of protection does not deter spam-bots. +
  • +
  • + Redirect users to a splash page telling them they are leaving your + website. While this is poor usability practice, it is often mandated + in corporate environments. +
  • +
+

+ Prior to HTML Purifier 3.1.1, this directive also enabled the munging + of browsable external resources, which could break things if your redirection + script was a splash page or used meta tags. To revert to + previous behavior, please use %URI.MungeResources. +

+

+ You may want to also use %URI.MungeSecretKey along with this directive + in order to enforce what URIs your redirector script allows. Open + redirector scripts can be a security risk and negatively affect the + reputation of your domain name. +

+

+ Starting with HTML Purifier 3.1.1, there is also these substitutions: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescriptionExample <a href="">
%r1 - The URI embeds a resource
(blank) - The URI is merely a link
%nThe name of the tag this URI came froma
%mThe name of the attribute this URI came fromhref
%pThe name of the CSS property this URI came from, or blank if irrelevant
+

+ Admittedly, these letters are somewhat arbitrary; the only stipulation + was that they couldn't be a through f. r is for resource (I would have preferred + e, but you take what you can get), n is for name, m + was picked because it came after n (and I couldn't use a), p is for + property. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt new file mode 100644 index 0000000..6fce0fd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,17 @@ +URI.MungeResources +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ If true, any URI munging directives like %URI.Munge + will also apply to embedded resources, such as <img src="">. + Be careful enabling this directive if you have a redirector script + that does not use the Location HTTP header; all of your images + and other embedded resources will break. +

+

+ Warning: It is strongly advised you use this in conjunction + %URI.MungeSecretKey to mitigate the security risk of an open redirector. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt new file mode 100644 index 0000000..1e17c1d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,30 @@ +URI.MungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

+ This directive enables secure checksum generation along with %URI.Munge. + It should be set to a secure key that is not shared with anyone else. + The checksum can be placed in the URI using %t. Use of this checksum + affords an additional level of protection by allowing a redirector + to check if a URI has passed through HTML Purifier with this line: +

+ +
$checksum === hash_hmac("sha256", $url, $secret_key)
+ +

+ If the output is TRUE, the redirector script should accept the URI. +

+ +

+ Please note that it would still be possible for an attacker to procure + secure hashes en-mass by abusing your website's Preview feature or the + like, but this service affords an additional level of protection + that should be combined with website blacklisting. +

+ +

+ Remember this has no effect if %URI.Munge is not on. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt new file mode 100644 index 0000000..23331a4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt @@ -0,0 +1,9 @@ +URI.OverrideAllowedSchemes +TYPE: bool +DEFAULT: true +--DESCRIPTION-- +If this is set to true (which it is by default), you can override +%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the +registry. If false, you will also have to update that directive in order +to add more schemes. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeHosts.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeHosts.txt new file mode 100644 index 0000000..c32b52c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeHosts.txt @@ -0,0 +1,14 @@ +URI.SafeIframeHosts +TYPE: lookup/null +DEFAULT: null +--DESCRIPTION-- +

+ A whitelist which indicates what explicit hosts should be + allowed to embed iframe. See also %HTML.SafeIframeRegexp, + it has precedence over this config. Here are some example values: +

+
    +
  • www.youtube.com - Allow YouTube videos
  • +
  • maps.google.com - Allow Embedding a Google map
  • +
+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt new file mode 100644 index 0000000..7908483 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt @@ -0,0 +1,22 @@ +URI.SafeIframeRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ A PCRE regular expression that will be matched against an iframe URI. This is + a relatively inflexible scheme, but works well enough for the most common + use-case of iframes: embedded video. This directive only has an effect if + %HTML.SafeIframe is enabled. Here are some example values: +

+
    +
  • %^http://www.youtube.com/embed/% - Allow YouTube videos
  • +
  • %^http://player.vimeo.com/video/% - Allow Vimeo videos
  • +
  • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
  • +
+

+ Note that this directive does not give you enough granularity to, say, disable + all autoplay videos. Pipe up on the HTML Purifier forums if this + is a capability you want. +

+--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini new file mode 100644 index 0000000..5de4505 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini @@ -0,0 +1,3 @@ +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php new file mode 100644 index 0000000..d342995 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php @@ -0,0 +1,169 @@ + true) indexed by name. + * @type array + * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets + */ + public $lookup = array(); + + /** + * Synchronized list of defined content sets (keys of info). + * @type array + */ + protected $keys = array(); + /** + * Synchronized list of defined content values (values of info). + * @type array + */ + protected $values = array(); + + /** + * Merges in module's content sets, expands identifiers in the content + * sets and populates the keys, values and lookup member variables. + * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule + */ + public function __construct($modules) + { + if (!is_array($modules)) { + $modules = array($modules); + } + // populate content_sets based on module hints + // sorry, no way of overloading + foreach ($modules as $module) { + foreach ($module->content_sets as $key => $value) { + $temp = $this->convertToLookup($value); + if (isset($this->lookup[$key])) { + // add it into the existing content set + $this->lookup[$key] = array_merge($this->lookup[$key], $temp); + } else { + $this->lookup[$key] = $temp; + } + } + } + $old_lookup = false; + while ($old_lookup !== $this->lookup) { + $old_lookup = $this->lookup; + foreach ($this->lookup as $i => $set) { + $add = array(); + foreach ($set as $element => $x) { + if (isset($this->lookup[$element])) { + $add += $this->lookup[$element]; + unset($this->lookup[$i][$element]); + } + } + $this->lookup[$i] += $add; + } + } + + foreach ($this->lookup as $key => $lookup) { + $this->info[$key] = implode(' | ', array_keys($lookup)); + } + $this->keys = array_keys($this->info); + $this->values = array_values($this->info); + } + + /** + * Accepts a definition; generates and assigns a ChildDef for it + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + */ + public function generateChildDef(&$def, $module) + { + if (!empty($def->child)) { // already done! + return; + } + $content_model = $def->content_model; + if (is_string($content_model)) { + // Assume that $this->keys is alphanumeric + $def->content_model = preg_replace_callback( + '/\b(' . implode('|', $this->keys) . ')\b/', + array($this, 'generateChildDefCallback'), + $content_model + ); + //$def->content_model = str_replace( + // $this->keys, $this->values, $content_model); + } + $def->child = $this->getChildDef($def, $module); + } + + public function generateChildDefCallback($matches) + { + return $this->info[$matches[0]]; + } + + /** + * Instantiates a ChildDef based on content_model and content_model_type + * member variables in HTMLPurifier_ElementDef + * @note This will also defer to modules for custom HTMLPurifier_ChildDef + * subclasses that need content set expansion + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + * @return HTMLPurifier_ChildDef corresponding to ElementDef + */ + public function getChildDef($def, $module) + { + $value = $def->content_model; + if (is_object($value)) { + trigger_error( + 'Literal object child definitions should be stored in '. + 'ElementDef->child not ElementDef->content_model', + E_USER_NOTICE + ); + return $value; + } + switch ($def->content_model_type) { + case 'required': + return new HTMLPurifier_ChildDef_Required($value); + case 'optional': + return new HTMLPurifier_ChildDef_Optional($value); + case 'empty': + return new HTMLPurifier_ChildDef_Empty(); + case 'custom': + return new HTMLPurifier_ChildDef_Custom($value); + } + // defer to its module + $return = false; + if ($module->defines_child_def) { // save a func call + $return = $module->getChildDef($def); + } + if ($return !== false) { + return $return; + } + + throw new Exception( + 'Could not determine which ChildDef class to instantiate', + E_USER_ERROR + ); + } + + /** + * Converts a string list of elements separated by pipes into + * a lookup array. + * @param string $string List of elements + * @return array Lookup array of elements + */ + protected function convertToLookup($string) + { + $array = explode('|', str_replace(' ', '', $string)); + $ret = array(); + foreach ($array as $k) { + $ret[$k] = true; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php new file mode 100644 index 0000000..5a0e7b9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php @@ -0,0 +1,84 @@ +_storage)) { + throw new Exception("Name $name produces collision, cannot re-register"); + } + $this->_storage[$name] =& $ref; + } + + /** + * Retrieves a variable reference from the context. + * @param string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + if (!$ignore_error) { + throw new Exception("Attempted to retrieve non-existent variable $name"); + } + $var = null; // so we can return by reference + return $var; + } + return $this->_storage[$name]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + throw new Exception("Attempted to destroy non-existent variable $name"); + } + unset($this->_storage[$name]); + } + + /** + * Checks whether or not the variable exists. + * @param string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $context_array Assoc array of variables to load + */ + public function loadArray($context_array) + { + foreach ($context_array as $key => $discard) { + $this->register($key, $context_array[$key]); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php new file mode 100644 index 0000000..bc6d433 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php @@ -0,0 +1,55 @@ +setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php new file mode 100644 index 0000000..9aa8ff3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php @@ -0,0 +1,129 @@ +type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + public function generateKey($config) + { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + public function isOld($key, $config) + { + if (substr_count($key, ',') < 2) { + return true; + } + list($version, $hash, $revision) = explode(',', $key, 3); + $compare = version_compare($version, $config->version); + // version mismatch, is always old + if ($compare != 0) { + return true; + } + // versions match, ids match, check revision number + if ($hash == $config->getBatchSerial($this->type) && + $revision < $config->get($this->type . '.DefinitionRev')) { + return true; + } + return false; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param HTMLPurifier_Definition $def Definition object to check + * @return bool true if good, false if not + */ + public function checkDefType($def) + { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be careful implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php new file mode 100644 index 0000000..b57a51b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php @@ -0,0 +1,112 @@ +copy(); + // reference is necessary for mocks in PHP 4 + $decorator->cache =& $cache; + $decorator->type = $cache->type; + return $decorator; + } + + /** + * Cross-compatible clone substitute + * @return HTMLPurifier_DefinitionCache_Decorator + */ + public function copy() + { + return new HTMLPurifier_DefinitionCache_Decorator(); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function add($def, $config) + { + return $this->cache->add($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + return $this->cache->set($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + return $this->cache->replace($def, $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + return $this->cache->get($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function remove($config) + { + return $this->cache->remove($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function flush($config) + { + return $this->cache->flush($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function cleanup($config) + { + return $this->cache->cleanup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php new file mode 100644 index 0000000..4991777 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -0,0 +1,78 @@ +definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + $status = parent::set($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + $status = parent::replace($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + $key = $this->generateKey($config); + if (isset($this->definitions[$key])) { + return $this->definitions[$key]; + } + $this->definitions[$key] = parent::get($config); + return $this->definitions[$key]; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in new file mode 100644 index 0000000..b1fec8d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -0,0 +1,82 @@ +checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (file_exists($file)) { + return false; + } + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + public function set($def, $config) + { + if (!$this->checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + public function replace($def, $config) + { + if (!$this->checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool|HTMLPurifier_Config + */ + public function get($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unserialize(file_get_contents($file)); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function remove($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unlink($file); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function flush($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // Apparently, on some versions of PHP, readdir will return + // an empty string if you pass an invalid argument to readdir. + // So you need this test. See #49. + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + unlink($dir . '/' . $filename); + } + closedir($dh); + return true; + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function cleanup($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // See #49 (and above). + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + $key = substr($filename, 0, strlen($filename) - 4); + $file = $dir . '/' . $filename; + if ($this->isOld($key, $config) && file_exists($file)) { + unlink($file); + } + } + closedir($dh); + return true; + } + + /** + * Generates the file path to the serial file corresponding to + * the configuration and definition name + * @param HTMLPurifier_Config $config + * @return string + * @todo Make protected + */ + public function generateFilePath($config) + { + $key = $this->generateKey($config); + return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; + } + + /** + * Generates the path to the directory contain this cache's serial files + * @param HTMLPurifier_Config $config + * @return string + * @note No trailing slash + * @todo Make protected + */ + public function generateDirectoryPath($config) + { + $base = $this->generateBaseDirectoryPath($config); + return $base . '/' . $this->type; + } + + /** + * Generates path to base directory that contains all definition type + * serials + * @param HTMLPurifier_Config $config + * @return mixed|string + * @todo Make protected + */ + public function generateBaseDirectoryPath($config) + { + $base = $config->get('Cache.SerializerPath'); + $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; + return $base; + } + + /** + * Convenience wrapper function for file_put_contents + * @param string $file File name to write to + * @param string $data Data to write into file + * @param HTMLPurifier_Config $config + * @return int|bool Number of bytes written if success, or false if failure. + */ + private function _write($file, $data, $config) + { + $result = file_put_contents($file, $data); + if ($result !== false) { + // set permissions of the new file (no execute) + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod !== null) { + chmod($file, $chmod & 0666); + } + } + return $result; + } + + /** + * Prepares the directory that this type stores the serials in + * @param HTMLPurifier_Config $config + * @return bool True if successful + */ + private function _prepareDir($config) + { + $directory = $this->generateDirectoryPath($config); + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod === null) { + if (!@mkdir($directory) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + return true; + } + if (!is_dir($directory)) { + $base = $this->generateBaseDirectoryPath($config); + if (!is_dir($base)) { + trigger_error( + 'Base directory ' . $base . ' does not exist, + please create or change using %Cache.SerializerPath', + E_USER_WARNING + ); + return false; + } elseif (!$this->_testPermissions($base, $chmod)) { + return false; + } + if (!@mkdir($directory, $chmod) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + if (!$this->_testPermissions($directory, $chmod)) { + return false; + } + } elseif (!$this->_testPermissions($directory, $chmod)) { + return false; + } + return true; + } + + /** + * Tests permissions on a directory and throws out friendly + * error messages and attempts to chmod it itself if possible + * @param string $dir Directory path + * @param int $chmod Permissions + * @return bool True if directory is writable + */ + private function _testPermissions($dir, $chmod) + { + // early abort, if it is writable, everything is hunky-dory + if (is_writable($dir)) { + return true; + } + if (!is_dir($dir)) { + // generally, you'll want to handle this beforehand + // so a more specific error message can be given + trigger_error( + 'Directory ' . $dir . ' does not exist', + E_USER_WARNING + ); + return false; + } + if (function_exists('posix_getuid') && $chmod !== null) { + // POSIX system, we can give more specific advice + if (fileowner($dir) === posix_getuid()) { + // we can chmod it ourselves + $chmod = $chmod | 0700; + if (chmod($dir, $chmod)) { + return true; + } + } elseif (filegroup($dir) === posix_getgid()) { + $chmod = $chmod | 0070; + } else { + // PHP's probably running as nobody, it is + // not obvious how to fix this (777 is probably + // bad if you are multi-user), let the user figure it out + $chmod = null; + } + trigger_error( + 'Directory ' . $dir . ' not writable. ' . + ($chmod === null ? '' : 'Please chmod to ' . decoct($chmod)), + E_USER_WARNING + ); + } else { + // generic error message + trigger_error( + 'Directory ' . $dir . ' not writable, ' . + 'please alter file permissions', + E_USER_WARNING + ); + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README new file mode 100644 index 0000000..2e35c1c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README @@ -0,0 +1,3 @@ +This is a dummy file to prevent Git from ignoring this empty directory. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php new file mode 100644 index 0000000..3a0f461 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php @@ -0,0 +1,106 @@ + array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + public static function instance($prototype = null) + { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Registers a new definition cache object + * @param string $short Short name of cache object, for reference + * @param string $long Full class name of cache object, for construction + */ + public function register($short, $long) + { + $this->implementations[$short] = $long; + } + + /** + * Factory method that creates a cache object based on configuration + * @param string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + public function create($type, $config) + { + $method = $config->get('Cache.DefinitionImpl'); + if ($method === null) { + return new HTMLPurifier_DefinitionCache_Null($type); + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + if (isset($this->implementations[$method]) && + class_exists($class = $this->implementations[$method])) { + $cache = new $class($type); + } else { + if ($method != 'Serializer') { + trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + } + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + public function addDecorator($decorator) + { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php new file mode 100644 index 0000000..4acd06e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php @@ -0,0 +1,73 @@ +renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + public $dtdSystem; + + public function __construct( + $name = null, + $xml = true, + $modules = array(), + $tidyModules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + $this->dtdPublic = $dtd_public; + $this->dtdSystem = $dtd_system; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php new file mode 100644 index 0000000..9ad7b4b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php @@ -0,0 +1,142 @@ +doctypes[$doctype->name] = $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) { + continue; + } + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) { + unset($this->aliases[$name]); + } + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param string $doctype Name of doctype + * @return HTMLPurifier_Doctype Editable doctype object + */ + public function get($doctype) + { + if (isset($this->aliases[$doctype])) { + $doctype = $this->aliases[$doctype]; + } + if (!isset($this->doctypes[$doctype])) { + throw new Exception('Doctype ' . htmlspecialchars($doctype) . ' does not exist'); + $anon = new HTMLPurifier_Doctype($doctype); + return $anon; + } + return $this->doctypes[$doctype]; + } + + /** + * Creates a doctype based on a configuration object, + * will perform initialization on the doctype + * @note Use this function to get a copy of doctype that config + * can hold on to (this is necessary in order to tell + * Generator whether or not the current document is XML + * based or not). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + public function getDoctypeFromConfig($config) + { + // recommended test + $doctype = $config->get('HTML.Doctype'); + if (!empty($doctype)) { + return $doctype; + } + $doctype = $config->get('HTML.CustomDoctype'); + if (!empty($doctype)) { + return $doctype; + } + // backwards-compatibility + if ($config->get('HTML.XHTML')) { + $doctype = 'XHTML 1.0'; + } else { + $doctype = 'HTML 4.01'; + } + if ($config->get('HTML.Strict')) { + $doctype .= ' Strict'; + } else { + $doctype .= ' Transitional'; + } + return $doctype; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php new file mode 100644 index 0000000..57cfd2b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php @@ -0,0 +1,216 @@ +setup(), this array may also + * contain an array at index 0 that indicates which attribute + * collections to load into the full array. It may also + * contain string indentifiers in lieu of HTMLPurifier_AttrDef, + * see HTMLPurifier_AttrTypes on how they are expanded during + * HTMLPurifier_HTMLDefinition->setup() processing. + */ + public $attr = array(); + + // XXX: Design note: currently, it's not possible to override + // previously defined AttrTransforms without messing around with + // the final generated config. This is by design; a previous version + // used an associated list of attr_transform, but it was extremely + // easy to accidentally override other attribute transforms by + // forgetting to specify an index (and just using 0.) While we + // could check this by checking the index number and complaining, + // there is a second problem which is that it is not at all easy to + // tell when something is getting overridden. Combine this with a + // codebase where this isn't really being used, and it's perfect for + // nuking. + + /** + * List of tags HTMLPurifier_AttrTransform to be done before validation. + * @type array + */ + public $attr_transform_pre = array(); + + /** + * List of tags HTMLPurifier_AttrTransform to be done after validation. + * @type array + */ + public $attr_transform_post = array(); + + /** + * HTMLPurifier_ChildDef of this tag. + * @type HTMLPurifier_ChildDef + */ + public $child; + + /** + * Abstract string representation of internal ChildDef rules. + * @see HTMLPurifier_ContentSets for how this is parsed and then transformed + * into an HTMLPurifier_ChildDef. + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model; + + /** + * Value of $child->type, used to determine which ChildDef to use, + * used in combination with $content_model. + * @warning This must be lowercase + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model_type; + + /** + * Does the element have a content model (#PCDATA | Inline)*? This + * is important for chameleon ins and del processing in + * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't + * have to worry about this one. + * @type bool + */ + public $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. + * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() + * @type array + */ + public $required_attr = array(); + + /** + * Lookup table of tags excluded from all descendants of this tag. + * @type array + * @note SGML permits exclusions for all descendants, but this is + * not possible with DTDs or XML Schemas. W3C has elected to + * use complicated compositions of content_models to simulate + * exclusion for children, but we go the simpler, SGML-style + * route of flat-out exclusions, which correctly apply to + * all descendants and not just children. Note that the XHTML + * Modularization Abstract Modules are blithely unaware of such + * distinctions. + */ + public $excludes = array(); + + /** + * This tag is explicitly auto-closed by the following tags. + * @type array + */ + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + * @type string + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + * @type bool + */ + public $formatting; + + /** + * Low-level factory constructor for creating new standalone element defs + */ + public static function create($content_model, $content_model_type, $attr) + { + $def = new HTMLPurifier_ElementDef(); + $def->content_model = $content_model; + $def->content_model_type = $content_model_type; + $def->attr = $attr; + return $def; + } + + /** + * Merges the values of another element definition into this one. + * Values from the new element def take precedence if a value is + * not mergeable. + * @param HTMLPurifier_ElementDef $def + */ + public function mergeIn($def) + { + // later keys takes precedence + foreach ($def->attr as $k => $v) { + if ($k === 0) { + // merge in the includes + // sorry, no way to override an include + foreach ($v as $v2) { + $this->attr[0][] = $v2; + } + continue; + } + if ($v === false) { + if (isset($this->attr[$k])) { + unset($this->attr[$k]); + } + continue; + } + $this->attr[$k] = $v; + } + $this->_mergeAssocArray($this->excludes, $def->excludes); + $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); + $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); + + if (!empty($def->content_model)) { + $this->content_model = + str_replace("#SUPER", (string)$this->content_model, $def->content_model); + $this->child = false; + } + if (!empty($def->content_model_type)) { + $this->content_model_type = $def->content_model_type; + $this->child = false; + } + if (!is_null($def->child)) { + $this->child = $def->child; + } + if (!is_null($def->formatting)) { + $this->formatting = $def->formatting; + } + if ($def->descendants_are_inline) { + $this->descendants_are_inline = $def->descendants_are_inline; + } + } + + /** + * Merges one array into another, removes values which equal false + * @param $a1 Array by reference that is merged into + * @param $a2 Array that merges into $a1 + */ + private function _mergeAssocArray(&$a1, $a2) + { + foreach ($a2 as $k => $v) { + if ($v === false) { + if (isset($a1[$k])) { + unset($a1[$k]); + } + continue; + } + $a1[$k] = $v; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php new file mode 100644 index 0000000..910181b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php @@ -0,0 +1,615 @@ += $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Cleans a UTF-8 string for well-formedness and SGML validity + * + * It will parse according to UTF-8 and return a valid UTF8 string, with + * non-SGML codepoints excluded. + * + * Specifically, it will permit: + * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} + * Source: https://www.w3.org/TR/REC-xml/#NT-Char + * Arguably this function should be modernized to the HTML5 set + * of allowed characters: + * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream + * which simultaneously expand and restrict the set of allowed characters. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @note Just for reference, the non-SGML code points are 0 to 31 and + * 127 to 159, inclusive. However, we allow code points 9, 10 + * and 13, which are the tab, line feed and carriage return + * respectively. 128 and above the code points map to multibyte + * UTF-8 representations. + * + * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and + * hsivonen@iki.fi at under the + * LGPL license. Notes on what changed are inside, but in general, + * the original code transformed UTF-8 text into an array of integer + * Unicode codepoints. Understandably, transforming that back to + * a string would be somewhat expensive, so the function was modded to + * directly operate on the string. However, this discourages code + * reuse, and the logic enumerated here would be useful for any + * function that needs to be able to understand UTF-8 characters. + * As of right now, only smart lossless character encoding converters + * would need that, and I'm probably not going to implement them. + */ + public static function cleanUTF8($str, $force_php = false) + { + // UTF-8 validity is checked since PHP 4.3.5 + // This is an optimization: if the string is already valid UTF-8, no + // need to do PHP stuff. 99% of the time, this will be the case. + if (preg_match( + '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', + $str + )) { + return $str; + } + + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + // original code involved an $out that was an array of Unicode + // codepoints. Instead of having to convert back into UTF-8, we've + // decided to directly append valid UTF-8 characters onto a string + // $out once they're done. $char accumulates raw bytes, while $mUcs4 + // turns into the Unicode code point, so there's some redundancy. + + $out = ''; + $char = ''; + + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $in = ord($str[$i]); + $char .= $str[$i]; // append byte to char + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character + // or a multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + if (($in <= 31 || $in == 127) && + !($in == 9 || $in == 13 || $in == 10) // save \r\t\n + ) { + // control characters, remove + } else { + $out .= $char; + } + // reset + $char = ''; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be + // either: + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on + // until the end of the sequence and let the later error + // handling code catch it. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 + // octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + // Current octet is neither in the US-ASCII range nor a + // legal first octet of a multi-octet sequence. + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // When mState is non-zero, we expect a continuation of the + // multi-octet sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + if (0 == --$mState) { + // End of the multi-octet sequence. mUcs4 now contains + // the final Unicode codepoint to be output + + // Check for illegal sequences and codepoints. + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters = illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ) { + + } elseif (0xFEFF != $mUcs4 && // omit BOM + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) + ) { + $out .= $char; + } + // initialize UTF8 cache (reset) + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + // used to result in complete fail, but we'll reset + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char =''; + } + } + } + return $out; + } + + /** + * Translates a Unicode codepoint into its corresponding UTF-8 character. + * @note Based on Feyd's function at + * , + * which is in public domain. + * @note While we're going to do code point parsing anyway, a good + * optimization would be to refuse to translate code points that + * are non-SGML characters. However, this could lead to duplication. + * @note This is very similar to the unichr function in + * maintenance/generate-entity-file.php (although this is superior, + * due to its sanity checks). + */ + + // +----------+----------+----------+----------+ + // | 33222222 | 22221111 | 111111 | | + // | 10987654 | 32109876 | 54321098 | 76543210 | bit + // +----------+----------+----------+----------+ + // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F + // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF + // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF + // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF + // +----------+----------+----------+----------+ + // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) + // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes + // +----------+----------+----------+----------+ + + public static function unichr($code) + { + if ($code > 1114111 or $code < 0 or + ($code >= 55296 and $code <= 57343) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return ''; + } + + $x = $y = $z = $w = 0; + if ($code < 128) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 63) | 128; + if ($code < 2048) { + $y = (($code & 2047) >> 6) | 192; + } else { + $y = (($code & 4032) >> 6) | 128; + if ($code < 65536) { + $z = (($code >> 12) & 15) | 224; + } else { + $z = (($code >> 12) & 63) | 128; + $w = (($code >> 18) & 7) | 240; + } + } + } + // set up the actual character + $ret = ''; + if ($w) { + $ret .= chr($w); + } + if ($z) { + $ret .= chr($z); + } + if ($y) { + $ret .= chr($y); + } + $ret .= chr($x); + + return $ret; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public static function convertToUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + throw new Exception('Invalid encoding ' . $encoding); + return ''; + } + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1' && function_exists('mb_convert_encoding')) { + $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1'); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + throw new Exception('Encoding not supported, please install iconv'); + } else { + throw new Exception( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541' + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @note Currently, this is a lossy conversion, with unexpressable + * characters being omitted. + */ + public static function convertFromUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) { + $clear_fix[$utf8] = ''; + } + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff + $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1' && function_exists('mb_convert_encoding')) { + $str = mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8'); + return $str; + } + throw new Exception('Encoding not supported'); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string ASCII encoded string with non-ASCII character entity-ized + * @warning Adapted from MediaWiki, claiming fair use: this is a common + * algorithm. If you disagree with this license fudgery, + * implement it yourself. + * @note Uses decimal numeric entities since they are best supported. + * @note This is a DUMB function: it has no concept of keeping + * character entities that the projected character encoding + * can allow. We could possibly implement a smart version + * but that would require it to also know which Unicode + * codepoints the charset supported (not an easy task). + * @note Sort of with cleanUTF8() but it assumes that $str is + * well-formed UTF-8 + */ + public static function convertToASCIIDumbLossless($str) + { + $bytesleft = 0; + $result = ''; + $working = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $bytevalue = ord($str[$i]); + if ($bytevalue <= 0x7F) { //0xxx xxxx + $result .= chr($bytevalue); + $bytesleft = 0; + } elseif ($bytevalue <= 0xBF) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if ($bytesleft <= 0) { + $result .= "&#" . $working . ";"; + } + } elseif ($bytevalue <= 0xDF) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif ($bytevalue <= 0xEF) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + throw new Exception( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()' + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) { + return $encodings[$encoding]; + } + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) { + return array(); + } + } + $ret = array(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ($r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) + ) { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php new file mode 100644 index 0000000..f12ff13 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php @@ -0,0 +1,48 @@ +table = unserialize(file_get_contents($file)); + } + + /** + * Retrieves sole instance of the object. + * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. + * @return HTMLPurifier_EntityLookup + */ + public static function instance($prototype = false) + { + // no references, since PHP doesn't copy unless modified + static $instance = null; + if ($prototype) { + $instance = $prototype; + } elseif (!$instance) { + $instance = new HTMLPurifier_EntityLookup(); + $instance->setup(); + } + return $instance; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser new file mode 100644 index 0000000..e8b0812 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser @@ -0,0 +1 @@ +a:253:{s:4:"fnof";s:2:"ƒ";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Β";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Ν";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Υ";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"π";s:3:"rho";s:2:"ρ";s:6:"sigmaf";s:2:"ς";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"τ";s:7:"upsilon";s:2:"υ";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"ϑ";s:5:"upsih";s:2:"ϒ";s:3:"piv";s:2:"ϖ";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"⁄";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"ℑ";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"™";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"←";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"⇐";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"∏";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"∝";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:6:"there4";s:3:"∴";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"⋅";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"◊";s:6:"spades";s:3:"♠";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Œ";s:5:"oelig";s:2:"œ";s:6:"Scaron";s:2:"Š";s:6:"scaron";s:2:"š";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"˜";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"‌";s:3:"zwj";s:3:"‍";s:3:"lrm";s:3:"‎";s:3:"rlm";s:3:"‏";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"”";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"­";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:4:"sup2";s:2:"²";s:4:"sup3";s:2:"³";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";s:2:"¾";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Á";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Å";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"Ì";s:6:"Iacute";s:2:"Í";s:5:"Icirc";s:2:"Î";s:4:"Iuml";s:2:"Ï";s:3:"ETH";s:2:"Ð";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ò";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ý";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"å";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";} \ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php new file mode 100644 index 0000000..1dcd10c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php @@ -0,0 +1,285 @@ +_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; + + $this->_textEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + "($semi_optional)". + ')/'; + + $this->_attrEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + // don't match if trailing is equals or alphanumeric (URL + // like) + "($semi_optional)(?![=;A-Za-z0-9])". + ')/'; + + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * textual data in an HTML document (as opposed to attributes.) + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteTextEntities($string) + { + return preg_replace_callback( + $this->_textEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * attribute contents in documents. + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteAttrEntities($string) + { + return preg_replace_callback( + $this->_attrEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function entityCallback($matches) + { + $entity = $matches[0]; + $hex_part = isset($matches[1]) ? $matches[1] : null; + $dec_part = isset($matches[2]) ? $matches[2] : null; + $named_part = empty($matches[3]) ? (empty($matches[4]) ? "" : $matches[4]) : $matches[3]; + if ($hex_part !== NULL && $hex_part !== "") { + return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); + } elseif ($dec_part !== NULL && $dec_part !== "") { + return HTMLPurifier_Encoder::unichr((int) $dec_part); + } else { + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$named_part])) { + return $this->_entity_lookup->table[$named_part]; + } else { + // exact match didn't match anything, so test if + // any of the semicolon optional match the prefix. + // Test that this is an EXACT match is important to + // prevent infinite loop + if (!empty($matches[3])) { + return preg_replace_callback( + $this->_semiOptionalPrefixRegex, + array($this, 'entityCallback'), + $entity + ); + } + return $entity; + } + } + } + + // LEGACY CODE BELOW + + /** + * Callback regex string for parsing entities. + * @type string + */ + protected $_substituteEntitiesRegex = + '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; + // 1. hex 2. dec 3. string (XML style) + + /** + * Decimal to parsed string conversion table for special entities. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + protected $_special_ent2dec = + array( + 'quot' => 34, + 'amp' => 38, + 'lt' => 60, + 'gt' => 62 + ); + + /** + * Substitutes non-special entities with their parsed equivalents. Since + * running this whenever you have parsed character is t3h 5uck, we run + * it before everything else. + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteNonSpecialEntities($string) + { + // it will try to detect missing semicolons, but don't rely on it + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'nonSpecialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function nonSpecialEntityCallback($matches) + { + // replaces all but big five + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + // abort for special characters + if (isset($this->_special_dec2str[$code])) { + return $entity; + } + return HTMLPurifier_Encoder::unichr($code); + } else { + if (isset($this->_special_ent2dec[$matches[3]])) { + return $entity; + } + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$matches[3]])) { + return $this->_entity_lookup->table[$matches[3]]; + } else { + return $entity; + } + } + } + + /** + * Substitutes only special entities with their parsed equivalents. + * + * @notice We try to avoid calling this function because otherwise, it + * would have to be called a lot (for every parsed section). + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteSpecialEntities($string) + { + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'specialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteSpecialEntities() that does the work. + * + * This callback has same syntax as nonSpecialEntityCallback(). + * + * @param array $matches PCRE-style matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + protected function specialEntityCallback($matches) + { + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + return isset($this->_special_dec2str[$int]) ? + $this->_special_dec2str[$int] : + $entity; + } else { + return isset($this->_special_ent2dec[$matches[3]]) ? + $this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] : + $entity; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php new file mode 100644 index 0000000..d47e3f2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php @@ -0,0 +1,244 @@ +locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; + } + + /** + * Sends an error message to the collector for later use + * @param int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + public function send($severity, $msg) + { + $args = array(); + if (func_num_args() > 2) { + $args = func_get_args(); + array_shift($args); + unset($args[0]); + } + + $token = $this->context->get('CurrentToken', true); + $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); + $attr = $this->context->get('CurrentAttr', true); + + // perform special substitutions, also add custom parameters + $subst = array(); + if (!is_null($token)) { + $args['CurrentToken'] = $token; + } + if (!is_null($attr)) { + $subst['$CurrentAttr.Name'] = $attr; + if (isset($token->attr[$attr])) { + $subst['$CurrentAttr.Value'] = $token->attr[$attr]; + } + } + + if (empty($args)) { + $msg = $this->locale->getMessage($msg); + } else { + $msg = $this->locale->formatMessage($msg, $args); + } + + if (!empty($subst)) { + $msg = strtr($msg, $subst); + } + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + // NEW CODE BELOW ... + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) { + $new_struct->value = clone $token; + } + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); + } + + /** + * Retrieves raw error data for custom formatter to use + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + public function getHTMLFormatted($config, $errors = null) + { + $ret = array(); + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) { + $errors = $this->errors; + } + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) { + continue; + } + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); + } + } + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); + } + + if (empty($errors)) { + return '

' . $this->locale->getMessage('ErrorCollector: No errors') . '

'; + } else { + return '
  • ' . implode('
  • ', $ret) . '
'; + } + + } + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) + { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '
'; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
'; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
';
+                //$string .= '';
+                //$string .= '
'; + $ret[] = $string; + } + foreach ($current->children as $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php new file mode 100644 index 0000000..cf869d3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php @@ -0,0 +1,74 @@ +children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php new file mode 100644 index 0000000..be85b4c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php @@ -0,0 +1,12 @@ +preFilter, + * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, + * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step + */ + +class HTMLPurifier_Filter +{ + + /** + * Name of the filter for identification purposes. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php new file mode 100644 index 0000000..e7e3cac --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -0,0 +1,362 @@ + blocks from input HTML, cleans them up + * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') + * so they can be used elsewhere in the document. + * + * @note + * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for + * sample usage. + * + * @note + * This filter can also be used on stylesheets not included in the + * document--something purists would probably prefer. Just directly + * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() + */ +class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter +{ + /** + * @type string + */ + public $name = 'ExtractStyleBlocks'; + + /** + * @type array + */ + private $_styleMatches = array(); + + /** + * @type csstidy + */ + private $_tidy; + + /** + * @type HTMLPurifier_AttrDef_HTML_ID + */ + private $_id_attrdef; + + /** + * @type HTMLPurifier_AttrDef_CSS_Ident + */ + private $_class_attrdef; + + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_enum_attrdef; + + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_universal_attrdef; + + public function __construct() + { + $this->_tidy = new csstidy(); + $this->_tidy->set_cfg('lowercase_s', false); + $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); + $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); + $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'first-child', + 'link', + 'visited', + 'active', + 'hover', + 'focus' + ) + ); + $this->_universal_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'initial', + 'inherit', + 'unset', + ) + ); + } + + /** + * Save the contents of CSS blocks to style matches + * @param array $matches preg_replace style $matches array + */ + protected function styleCallback($matches) + { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline + // we must not grab foo in a font-family prop). + if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { + $css = str_replace( + array('<', '>', '&'), + array('\3C ', '\3E ', '\26 '), + $css + ); + } + return $css; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php new file mode 100644 index 0000000..d86509c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php @@ -0,0 +1,65 @@ +]+>.+?' . + '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; + $pre_replace = '\1'; + return preg_replace($pre_regex, $pre_replace, (string)$html); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), (string)$html); + } + + /** + * @param $url + * @return string + */ + protected function armorUrl($url) + { + return str_replace('--', '--', $url); + } + + /** + * @param array $matches + * @return string + */ + protected function postFilterCallback($matches) + { + $url = $this->armorUrl($matches[1]); + return '' . + '' . + '' . + ''; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php new file mode 100644 index 0000000..457fa90 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php @@ -0,0 +1,286 @@ + tags. + * @type bool + */ + private $_scriptFix = false; + + /** + * Cache of HTMLDefinition during HTML output to determine whether or + * not attributes should be minimized. + * @type HTMLPurifier_HTMLDefinition + */ + private $_def; + + /** + * Cache of %Output.SortAttr. + * @type bool + */ + private $_sortAttr; + + /** + * Cache of %Output.FlashCompat. + * @type bool + */ + private $_flashCompat; + + /** + * Cache of %Output.FixInnerHTML. + * @type bool + */ + private $_innerHTMLFix; + + /** + * Stack for keeping track of object information when outputting IE + * compatibility code. + * @type array + */ + private $_flashStack = array(); + + /** + * Configuration for the generator + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + public function __construct($config, $context) + { + $this->config = $config; + $this->_scriptFix = $config->get('Output.CommentScriptContents'); + $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); + $this->_sortAttr = $config->get('Output.SortAttr'); + $this->_flashCompat = $config->get('Output.FlashCompat'); + $this->_def = $config->getHTMLDefinition(); + $this->_xhtml = $this->_def->doctype->xml; + } + + /** + * Generates HTML from an array of tokens. + * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token + * @return string Generated HTML + */ + public function generateFromTokens($tokens) + { + if (!$tokens) { + return ''; + } + + // Basic algorithm + $html = ''; + for ($i = 0, $size = count($tokens); $i < $size; $i++) { + if ($this->_scriptFix && $tokens[$i]->name === 'script' + && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) { + // script special case + // the contents of the script block must be ONE token + // for this to work. + $html .= $this->generateFromToken($tokens[$i++]); + $html .= $this->generateScriptFromToken($tokens[$i++]); + } + $html .= $this->generateFromToken($tokens[$i]); + } + + // Tidy cleanup + if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) { + $tidy = new Tidy; + $tidy->parseString( + $html, + array( + 'indent'=> true, + 'output-xhtml' => $this->_xhtml, + 'show-body-only' => true, + 'indent-spaces' => 2, + 'wrap' => 68, + ), + 'utf8' + ); + $tidy->cleanRepair(); + $html = (string) $tidy; // explicit cast necessary + } + + // Normalize newlines to system defined value + if ($this->config->get('Core.NormalizeNewlines')) { + $nl = $this->config->get('Output.Newline'); + if ($nl === null) { + $nl = PHP_EOL; + } + if ($nl !== "\n") { + $html = str_replace("\n", $nl, $html); + } + } + return $html; + } + + /** + * Generates HTML from a single token. + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string Generated HTML + */ + public function generateFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token) { + trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING); + return ''; + + } elseif ($token instanceof HTMLPurifier_Token_Start) { + $attr = $this->generateAttributes($token->attr, $token->name); + if ($this->_flashCompat) { + if ($token->name == "object") { + $flash = new stdClass(); + $flash->attr = $token->attr; + $flash->param = array(); + $this->_flashStack[] = $flash; + } + } + return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_End) { + $_extra = ''; + if ($this->_flashCompat) { + if ($token->name == "object" && !empty($this->_flashStack)) { + // doesn't do anything for now + } + } + return $_extra . 'name . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { + $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; + } + $attr = $this->generateAttributes($token->attr, $token->name); + return '<' . $token->name . ($attr ? ' ' : '') . $attr . + ( $this->_xhtml ? ' /': '' ) //
v.
+ . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Text) { + return $this->escape($token->data, ENT_NOQUOTES); + + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + return ''; + } else { + return ''; + + } + } + + /** + * Special case processor for the contents of script tags + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string + * @warning This runs into problems if there's already a literal + * --> somewhere inside the script contents. + */ + public function generateScriptFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token_Text) { + return $this->generateFromToken($token); + } + // Thanks + $data = preg_replace('#//\s*$#', '', $token->data); + return ''; + } + + /** + * Generates attribute declarations from attribute array. + * @note This does not include the leading or trailing space. + * @param array $assoc_array_of_attributes Attribute array + * @param string $element Name of element attributes are for, used to check + * attribute minimization. + * @return string Generated HTML fragment for insertion. + */ + public function generateAttributes($assoc_array_of_attributes, $element = '') + { + $html = ''; + if ($this->_sortAttr) { + ksort($assoc_array_of_attributes); + } + foreach ($assoc_array_of_attributes as $key => $value) { + if (!$this->_xhtml) { + // Remove namespaced attributes + if (strpos($key, ':') !== false) { + continue; + } + // Check if we should minimize the attribute: val="val" -> val + if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { + $html .= $key . ' '; + continue; + } + } + // Workaround for Internet Explorer innerHTML bug. + // Essentially, Internet Explorer, when calculating + // innerHTML, omits quotes if there are no instances of + // angled brackets, quotes or spaces. However, when parsing + // HTML (for example, when you assign to innerHTML), it + // treats backticks as quotes. Thus, + // `` + // becomes + // `` + // becomes + // + // Fortunately, all we need to do is trigger an appropriate + // quoting style, which we do by adding an extra space. + // This also is consistent with the W3C spec, which states + // that user agents may ignore leading or trailing + // whitespace (in fact, most don't, at least for attributes + // like alt, but an extra space at the end is barely + // noticeable). Still, we have a configuration knob for + // this, since this transformation is not necessary if you + // don't process user input with innerHTML or you don't plan + // on supporting Internet Explorer. + if ($this->_innerHTMLFix) { + if (strpos($value, '`') !== false) { + // check if correct quoting style would not already be + // triggered + if (strcspn($value, '"\' <>') === strlen($value)) { + // protect! + $value .= ' '; + } + } + } + $html .= $key.'="'.$this->escape($value).'" '; + } + return rtrim($html); + } + + /** + * Escapes raw text data. + * @todo This really ought to be protected, but until we have a facility + * for properly generating HTML here w/o using tokens, it stays + * public. + * @param string $string String data to escape for HTML. + * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is + * permissible for non-attribute output. + * @return string escaped data. + */ + public function escape($string, $quote = null) + { + // Workaround for APC bug on Mac Leopard reported by sidepodcast + // http://htmlpurifier.org/phorum/read.php?3,4823,4846 + if ($quote === null) { + $quote = ENT_COMPAT; + } + return htmlspecialchars($string, $quote, 'UTF-8'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php new file mode 100644 index 0000000..dc2c33c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php @@ -0,0 +1,488 @@ +getAnonymousModule(); + if (!isset($module->info[$element_name])) { + $element = $module->addBlankElement($element_name); + } else { + $element = $module->info[$element_name]; + } + $element->attr[$attr_name] = $def; + } + + /** + * Adds a custom element to your HTML definition + * @see HTMLPurifier_HTMLModule::addElement() for detailed + * parameter and return value descriptions. + */ + public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) + { + $module = $this->getAnonymousModule(); + // assume that if the user is calling this, the element + // is safe. This may not be a good idea + $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); + return $element; + } + + /** + * Adds a blank element to your HTML definition, for overriding + * existing behavior + * @param string $element_name + * @return HTMLPurifier_ElementDef + * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed + * parameter and return value descriptions. + */ + public function addBlankElement($element_name) + { + $module = $this->getAnonymousModule(); + $element = $module->addBlankElement($element_name); + return $element; + } + + /** + * Retrieves a reference to the anonymous module, so you can + * bust out advanced features without having to make your own + * module. + * @return HTMLPurifier_HTMLModule + */ + public function getAnonymousModule() + { + if (!$this->_anonModule) { + $this->_anonModule = new HTMLPurifier_HTMLModule(); + $this->_anonModule->name = 'Anonymous'; + } + return $this->_anonModule; + } + + private $_anonModule = null; + + // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- + + /** + * @type string + */ + public $type = 'HTML'; + + /** + * @type HTMLPurifier_HTMLModuleManager + */ + public $manager; + + /** + * Performs low-cost, preliminary initialization. + */ + public function __construct() + { + $this->manager = new HTMLPurifier_HTMLModuleManager(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->processModules($config); + $this->setupConfigStuff($config); + unset($this->manager); + + // cleanup some of the element definitions + foreach ($this->info as $k => $v) { + unset($this->info[$k]->content_model); + unset($this->info[$k]->content_model_type); + } + } + + /** + * Extract out the information from the manager + * @param HTMLPurifier_Config $config + */ + protected function processModules($config) + { + if ($this->_anonModule) { + // for user specific changes + // this is late-loaded so we don't have to deal with PHP4 + // reference wonky-ness + $this->manager->addModule($this->_anonModule); + unset($this->_anonModule); + } + + $this->manager->setup($config); + $this->doctype = $this->manager->doctype; + + foreach ($this->manager->modules as $module) { + foreach ($module->info_tag_transform as $k => $v) { + if ($v === false) { + unset($this->info_tag_transform[$k]); + } else { + $this->info_tag_transform[$k] = $v; + } + } + foreach ($module->info_attr_transform_pre as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_pre[$k]); + } else { + $this->info_attr_transform_pre[$k] = $v; + } + } + foreach ($module->info_attr_transform_post as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_post[$k]); + } else { + $this->info_attr_transform_post[$k] = $v; + } + } + foreach ($module->info_injector as $k => $v) { + if ($v === false) { + unset($this->info_injector[$k]); + } else { + $this->info_injector[$k] = $v; + } + } + } + $this->info = $this->manager->getElements(); + $this->info_content_sets = $this->manager->contentSets->lookup; + } + + /** + * Sets up stuff based on config. We need a better way of doing this. + * @param HTMLPurifier_Config $config + */ + protected function setupConfigStuff($config) + { + $block_wrapper = $config->get('HTML.BlockWrapper'); + if (isset($this->info_content_sets['Block'][$block_wrapper])) { + $this->info_block_wrapper = $block_wrapper; + } else { + throw new Exception( + 'Cannot use non-block element as block wrapper' + ); + } + + $parent = $config->get('HTML.Parent'); + $def = $this->manager->getElement($parent, true); + if ($def) { + $this->info_parent = $parent; + $this->info_parent_def = $def; + } else { + throw new Exception('Cannot use unrecognized element as parent'); + } + + // support template text + $support = "(for information on implementing this, see the support forums) "; + + // setup allowed elements ----------------------------------------- + + $allowed_elements = $config->get('HTML.AllowedElements'); + $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early + + if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { + $allowed = $config->get('HTML.Allowed'); + if (is_string($allowed)) { + list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); + } + } + + if (is_array($allowed_elements)) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_elements[$name])) { + unset($this->info[$name]); + } + unset($allowed_elements[$name]); + } + // emit errors + foreach ($allowed_elements as $element => $d) { + $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! + trigger_error("Element '$element' is not supported $support", E_USER_WARNING); + } + } + + // setup allowed attributes --------------------------------------- + + $allowed_attributes_mutable = $allowed_attributes; // by copy! + if (is_array($allowed_attributes)) { + // This actually doesn't do anything, since we went away from + // global attributes. It's possible that userland code uses + // it, but HTMLModuleManager doesn't! + foreach ($this->info_global_attr as $attr => $x) { + $keys = array($attr, "*@$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + unset($this->info_global_attr[$attr]); + } + } + + foreach ($this->info as $tag => $info) { + foreach ($info->attr as $attr => $x) { + $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + if ($this->info[$tag]->attr[$attr]->required) { + trigger_error( + "Required attribute '$attr' in element '$tag' " . + "was not allowed, which means '$tag' will not be allowed either", + E_USER_WARNING + ); + } + unset($this->info[$tag]->attr[$attr]); + } + } + } + // emit errors + foreach ($allowed_attributes_mutable as $elattr => $d) { + $bits = preg_split('/[.@]/', $elattr, 2); + $c = count($bits); + switch ($c) { + case 2: + if ($bits[0] !== '*') { + $element = htmlspecialchars($bits[0]); + $attribute = htmlspecialchars($bits[1]); + if (!isset($this->info[$element])) { + trigger_error( + "Cannot allow attribute '$attribute' if element " . + "'$element' is not allowed/supported $support" + ); + } else { + trigger_error( + "Attribute '$attribute' in element '$element' not supported $support", + E_USER_WARNING + ); + } + break; + } + // otherwise fall through + case 1: + $attribute = htmlspecialchars($bits[0]); + trigger_error( + "Global attribute '$attribute' is not ". + "supported in any elements $support", + E_USER_WARNING + ); + break; + } + } + } + + // setup forbidden elements --------------------------------------- + + $forbidden_elements = $config->get('HTML.ForbiddenElements'); + $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); + + foreach ($this->info as $tag => $info) { + if (isset($forbidden_elements[$tag])) { + unset($this->info[$tag]); + continue; + } + foreach ($info->attr as $attr => $x) { + if (isset($forbidden_attributes["$tag@$attr"]) || + isset($forbidden_attributes["*@$attr"]) || + isset($forbidden_attributes[$attr]) + ) { + unset($this->info[$tag]->attr[$attr]); + continue; + } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually + // $tag.$attr are not user supplied, so no worries! + trigger_error( + "Error with $tag.$attr: tag.attr syntax not supported for " . + "HTML.ForbiddenAttributes; use tag@attr instead", + E_USER_WARNING + ); + } + } + } + foreach ($forbidden_attributes as $key => $v) { + if (strlen($key) < 2) { + continue; + } + if ($key[0] != '*') { + continue; + } + if ($key[1] == '.') { + trigger_error( + "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", + E_USER_WARNING + ); + } + } + + // setup injectors ----------------------------------------------------- + foreach ($this->info_injector as $i => $injector) { + if ($injector->checkNeeded($config) !== false) { + // remove injector that does not have it's required + // elements/attributes present, and is thus not needed. + unset($this->info_injector[$i]); + } + } + } + + /** + * Parses a TinyMCE-flavored Allowed Elements and Attributes list into + * separate lists for processing. Format is element[attr1|attr2],element2... + * @warning Although it's largely drawn from TinyMCE's implementation, + * it is different, and you'll probably have to modify your lists + * @param array $list String list to parse + * @return array + * @todo Give this its own class, probably static interface + */ + public function parseTinyMCEAllowedList($list) + { + $list = str_replace(array(' ', "\t"), '', $list); + + $elements = array(); + $attributes = array(); + + $chunks = preg_split('/(,|[\n\r]+)/', $list); + foreach ($chunks as $chunk) { + if (empty($chunk)) { + continue; + } + // remove TinyMCE element control characters + if (!strpos($chunk, '[')) { + $element = $chunk; + $attr = false; + } else { + list($element, $attr) = explode('[', $chunk); + } + if ($element !== '*') { + $elements[$element] = true; + } + if (!$attr) { + continue; + } + $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] + $attr = explode('|', $attr); + foreach ($attr as $key) { + $attributes["$element.$key"] = true; + } + } + return array($elements, $attributes); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php new file mode 100644 index 0000000..9dbb987 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php @@ -0,0 +1,285 @@ +info, since the object's data is only info, + * with extra behavior associated with it. + * @type array + */ + public $attr_collections = array(); + + /** + * Associative array of deprecated tag name to HTMLPurifier_TagTransform. + * @type array + */ + public $info_tag_transform = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed before validation. + * @type array + */ + public $info_attr_transform_pre = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed after validation. + * @type array + */ + public $info_attr_transform_post = array(); + + /** + * List of HTMLPurifier_Injector to be performed during well-formedness fixing. + * An injector will only be invoked if all of it's pre-requisites are met; + * if an injector fails setup, there will be no error; it will simply be + * silently disabled. + * @type array + */ + public $info_injector = array(); + + /** + * Boolean flag that indicates whether or not getChildDef is implemented. + * For optimization reasons: may save a call to a function. Be sure + * to set it if you do implement getChildDef(), otherwise it will have + * no effect! + * @type bool + */ + public $defines_child_def = false; + + /** + * Boolean flag whether or not this module is safe. If it is not safe, all + * of its members are unsafe. Modules are safe by default (this might be + * slightly dangerous, but it doesn't make much sense to force HTML Purifier, + * which is based off of safe HTML, to explicitly say, "This is safe," even + * though there are modules which are "unsafe") + * + * @type bool + * @note Previously, safety could be applied at an element level granularity. + * We've removed this ability, so in order to add "unsafe" elements + * or attributes, a dedicated module with this property set to false + * must be used. + */ + public $safe = true; + + /** + * Retrieves a proper HTMLPurifier_ChildDef subclass based on + * content_model and content_model_type member variables of + * the HTMLPurifier_ElementDef class. There is a similar function + * in HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef subclass + */ + public function getChildDef($def) + { + return false; + } + + // -- Convenience ----------------------------------------------------- + + /** + * Convenience function that sets up a new element + * @param string $element Name of element to add + * @param string|bool $type What content set should element be registered to? + * Set as false to skip this step. + * @param string|HTMLPurifier_ChildDef $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @param array|string $attr_includes What attribute collections to register to + * element? + * @param array $attr What unique attributes does the element define? + * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. + * @return HTMLPurifier_ElementDef Created element definition object, so you + * can set advanced parameters + */ + public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) + { + $this->elements[] = $element; + // parse content_model + list($content_model_type, $content_model) = $this->parseContents($contents); + // merge in attribute inclusions + $this->mergeInAttrIncludes($attr, $attr_includes); + // add element to content sets + if ($type) { + $this->addElementToContentSet($element, $type); + } + // create element + $this->info[$element] = HTMLPurifier_ElementDef::create( + $content_model, + $content_model_type, + $attr + ); + // literal object $contents means direct child manipulation + if (!is_string($contents)) { + $this->info[$element]->child = $contents; + } + return $this->info[$element]; + } + + /** + * Convenience function that creates a totally blank, non-standalone + * element. + * @param string $element Name of element to create + * @return HTMLPurifier_ElementDef Created element + */ + public function addBlankElement($element) + { + if (!isset($this->info[$element])) { + $this->elements[] = $element; + $this->info[$element] = new HTMLPurifier_ElementDef(); + $this->info[$element]->standalone = false; + } else { + trigger_error("Definition for $element already exists in module, cannot redefine"); + } + return $this->info[$element]; + } + + /** + * Convenience function that registers an element to a content set + * @param string $element Element to register + * @param string $type Name content set (warning: case sensitive, usually upper-case + * first letter) + */ + public function addElementToContentSet($element, $type) + { + if (!isset($this->content_sets[$type])) { + $this->content_sets[$type] = ''; + } else { + $this->content_sets[$type] .= ' | '; + } + $this->content_sets[$type] .= $element; + } + + /** + * Convenience function that transforms single-string contents + * into separate content model and content model type + * @param string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @return array + * @note If contents is an object, an array of two nulls will be + * returned, and the callee needs to take the original $contents + * and use it directly. + */ + public function parseContents($contents) + { + if (!is_string($contents)) { + return array(null, null); + } // defer + switch ($contents) { + // check for shorthand content model forms + case 'Empty': + return array('empty', ''); + case 'Inline': + return array('optional', 'Inline | #PCDATA'); + case 'Flow': + return array('optional', 'Flow | #PCDATA'); + } + list($content_model_type, $content_model) = explode(':', $contents); + $content_model_type = strtolower(trim($content_model_type)); + $content_model = trim($content_model); + return array($content_model_type, $content_model); + } + + /** + * Convenience function that merges a list of attribute includes into + * an attribute array. + * @param array $attr Reference to attr array to modify + * @param array $attr_includes Array of includes / string include to merge in + */ + public function mergeInAttrIncludes(&$attr, $attr_includes) + { + if (!is_array($attr_includes)) { + if (empty($attr_includes)) { + $attr_includes = array(); + } else { + $attr_includes = array($attr_includes); + } + } + $attr[0] = $attr_includes; + } + + /** + * Convenience function that generates a lookup table with boolean + * true as value. + * @param string $list List of values to turn into a lookup + * @note You can also pass an arbitrary number of arguments in + * place of the regular argument + * @return array array equivalent of list + */ + public function makeLookup($list) + { + $args = func_get_args(); + if (is_string($list)) { + $list = $args; + } + $ret = array(); + foreach ($list as $value) { + if (is_null($value)) { + continue; + } + $ret[$value] = true; + } + return $ret; + } + + /** + * Lazy load construction of the module after determining whether + * or not it's needed, and also when a finalized configuration object + * is available. + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php new file mode 100644 index 0000000..1e67c79 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php @@ -0,0 +1,44 @@ + array('dir' => false) + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $bdo = $this->addElement( + 'bdo', + 'Inline', + 'Inline', + array('Core', 'Lang'), + array( + 'dir' => 'Enum#ltr,rtl', // required + // The Abstract Module specification has the attribute + // inclusions wrong for bdo: bdo allows Lang + ) + ); + $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir(); + + $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php new file mode 100644 index 0000000..7220c14 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php @@ -0,0 +1,32 @@ + array( + 0 => array('Style'), + // 'xml:space' => false, + 'class' => 'Class', + 'id' => 'ID', + 'title' => 'CDATA', + 'contenteditable' => 'ContentEditable', + ), + 'Lang' => array(), + 'I18N' => array( + 0 => array('Lang'), // proprietary, for xml:lang/lang + ), + 'Common' => array( + 0 => array('Core', 'I18N') + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php new file mode 100644 index 0000000..f02a563 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php @@ -0,0 +1,55 @@ + 'URI', + // 'datetime' => 'Datetime', // not implemented + ); + $this->addElement('del', 'Inline', $contents, 'Common', $attr); + $this->addElement('ins', 'Inline', $contents, 'Common', $attr); + } + + // HTML 4.01 specifies that ins/del must not contain block + // elements when used in an inline context, chameleon is + // a complicated workaround to achieve this effect + + // Inline context ! Block context (exclamation mark is + // separator, see getChildDef for parsing) + + /** + * @type bool + */ + public $defines_child_def = true; + + /** + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef_Chameleon + */ + public function getChildDef($def) + { + if ($def->content_model_type != 'chameleon') { + return false; + } + $value = explode('!', $def->content_model); + return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php new file mode 100644 index 0000000..eb0edcf --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php @@ -0,0 +1,194 @@ + 'Form', + 'Inline' => 'Formctrl', + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + if ($config->get('HTML.Forms')) { + $this->safe = true; + } + + $form = $this->addElement( + 'form', + 'Form', + 'Required: Heading | List | Block | fieldset', + 'Common', + array( + 'accept' => 'ContentTypes', + 'accept-charset' => 'Charsets', + 'action*' => 'URI', + 'method' => 'Enum#get,post', + // really ContentType, but these two are the only ones used today + 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data', + ) + ); + $form->excludes = array('form' => true); + + $input = $this->addElement( + 'input', + 'Formctrl', + 'Empty', + 'Common', + array( + 'accept' => 'ContentTypes', + 'accesskey' => 'Character', + 'alt' => 'Text', + 'checked' => 'Bool#checked', + 'disabled' => 'Bool#disabled', + 'maxlength' => 'Number', + 'name' => 'CDATA', + 'readonly' => 'Bool#readonly', + 'size' => 'Number', + 'src' => 'URI#embedded', + 'tabindex' => 'Number', + 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image', + 'value' => 'CDATA', + ) + ); + $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input(); + + $this->addElement( + 'select', + 'Formctrl', + 'Required: optgroup | option', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'multiple' => 'Bool#multiple', + 'name' => 'CDATA', + 'size' => 'Number', + 'tabindex' => 'Number', + ) + ); + + $this->addElement( + 'option', + false, + 'Optional: #PCDATA', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'label' => 'Text', + 'selected' => 'Bool#selected', + 'value' => 'CDATA', + ) + ); + // It's illegal for there to be more than one selected, but not + // be multiple. Also, no selected means undefined behavior. This might + // be difficult to implement; perhaps an injector, or a context variable. + + $textarea = $this->addElement( + 'textarea', + 'Formctrl', + 'Optional: #PCDATA', + 'Common', + array( + 'accesskey' => 'Character', + 'cols*' => 'Number', + 'disabled' => 'Bool#disabled', + 'name' => 'CDATA', + 'readonly' => 'Bool#readonly', + 'rows*' => 'Number', + 'tabindex' => 'Number', + ) + ); + $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea(); + + $button = $this->addElement( + 'button', + 'Formctrl', + 'Optional: #PCDATA | Heading | List | Block | Inline', + 'Common', + array( + 'accesskey' => 'Character', + 'disabled' => 'Bool#disabled', + 'name' => 'CDATA', + 'tabindex' => 'Number', + 'type' => 'Enum#button,submit,reset', + 'value' => 'CDATA', + ) + ); + + // For exclusions, ideally we'd specify content sets, not literal elements + $button->excludes = $this->makeLookup( + 'form', + 'fieldset', // Form + 'input', + 'select', + 'textarea', + 'label', + 'button', // Formctrl + 'a', // as per HTML 4.01 spec, this is omitted by modularization + 'isindex', + 'iframe' // legacy items + ); + + // Extra exclusion: img usemap="" is not permitted within this element. + // We'll omit this for now, since we don't have any good way of + // indicating it yet. + + // This is HIGHLY user-unfriendly; we need a custom child-def for this + $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common'); + + $label = $this->addElement( + 'label', + 'Formctrl', + 'Optional: #PCDATA | Inline', + 'Common', + array( + 'accesskey' => 'Character', + // 'for' => 'IDREF', // IDREF not implemented, cannot allow + ) + ); + $label->excludes = array('label' => true); + + $this->addElement( + 'legend', + false, + 'Optional: #PCDATA | Inline', + 'Common', + array( + 'accesskey' => 'Character', + ) + ); + + $this->addElement( + 'optgroup', + false, + 'Required: option', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'label*' => 'Text', + ) + ); + // Don't forget an injector for . This one's a little complex + // because it maps to multiple elements. + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php new file mode 100644 index 0000000..72d7a31 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php @@ -0,0 +1,40 @@ +addElement( + 'a', + 'Inline', + 'Inline', + 'Common', + array( + // 'accesskey' => 'Character', + // 'charset' => 'Charset', + 'href' => 'URI', + // 'hreflang' => 'LanguageCode', + 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), + 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), + // 'tabindex' => 'Number', + // 'type' => 'ContentType', + ) + ); + $a->formatting = true; + $a->excludes = array('a' => true); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php new file mode 100644 index 0000000..71dfc77 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php @@ -0,0 +1,57 @@ +get('HTML.SafeIframe')) { + $this->safe = true; + } + $attrs = array( + 'src' => 'URI#embedded', + 'width' => 'Length', + 'height' => 'Length', + 'name' => 'ID', + 'scrolling' => 'Enum#yes,no,auto', + 'frameborder' => 'Enum#0,1', + 'longdesc' => 'URI', + 'marginheight' => 'Pixels', + 'marginwidth' => 'Pixels', + ); + + if ($config->get('HTML.Trusted')) { + $attrs['allowfullscreen'] = 'Bool#allowfullscreen'; + } + + $this->addElement( + 'iframe', + 'Inline', + 'Flow', + 'Common', + $attrs + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php new file mode 100644 index 0000000..0f5fdb3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php @@ -0,0 +1,49 @@ +get('HTML.MaxImgLength'); + $img = $this->addElement( + 'img', + 'Inline', + 'Empty', + 'Common', + array( + 'alt*' => 'Text', + // According to the spec, it's Length, but percents can + // be abused, so we allow only Pixels. + 'height' => 'Pixels#' . $max, + 'width' => 'Pixels#' . $max, + 'longdesc' => 'URI', + 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded + ) + ); + if ($max === null || $config->get('HTML.Trusted')) { + $img->attr['height'] = + $img->attr['width'] = 'Length'; + } + + // kind of strange, but splitting things up would be inefficient + $img->attr_transform_pre[] = + $img->attr_transform_post[] = + new HTMLPurifier_AttrTransform_ImgRequired(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php new file mode 100644 index 0000000..86b5299 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php @@ -0,0 +1,186 @@ +addElement( + 'basefont', + 'Inline', + 'Empty', + null, + array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + 'id' => 'ID' + ) + ); + $this->addElement('center', 'Block', 'Flow', 'Common'); + $this->addElement( + 'dir', + 'Block', + 'Required: li', + 'Common', + array( + 'compact' => 'Bool#compact' + ) + ); + $this->addElement( + 'font', + 'Inline', + 'Inline', + array('Core', 'I18N'), + array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + ) + ); + $this->addElement( + 'menu', + 'Block', + 'Required: li', + 'Common', + array( + 'compact' => 'Bool#compact' + ) + ); + + $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); + $s->formatting = true; + + $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); + $strike->formatting = true; + + $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); + $u->formatting = true; + + // setup modifications to old elements + + $align = 'Enum#left,right,center,justify'; + + $address = $this->addBlankElement('address'); + $address->content_model = 'Inline | #PCDATA | p'; + $address->content_model_type = 'optional'; + $address->child = false; + + $blockquote = $this->addBlankElement('blockquote'); + $blockquote->content_model = 'Flow | #PCDATA'; + $blockquote->content_model_type = 'optional'; + $blockquote->child = false; + + $br = $this->addBlankElement('br'); + $br->attr['clear'] = 'Enum#left,all,right,none'; + + $caption = $this->addBlankElement('caption'); + $caption->attr['align'] = 'Enum#top,bottom,left,right'; + + $div = $this->addBlankElement('div'); + $div->attr['align'] = $align; + + $dl = $this->addBlankElement('dl'); + $dl->attr['compact'] = 'Bool#compact'; + + for ($i = 1; $i <= 6; $i++) { + $h = $this->addBlankElement("h$i"); + $h->attr['align'] = $align; + } + + $hr = $this->addBlankElement('hr'); + $hr->attr['align'] = $align; + $hr->attr['noshade'] = 'Bool#noshade'; + $hr->attr['size'] = 'Pixels'; + $hr->attr['width'] = 'Length'; + + $img = $this->addBlankElement('img'); + $img->attr['align'] = 'IAlign'; + $img->attr['border'] = 'Pixels'; + $img->attr['hspace'] = 'Pixels'; + $img->attr['vspace'] = 'Pixels'; + + // figure out this integer business + + $li = $this->addBlankElement('li'); + $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); + $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; + + $ol = $this->addBlankElement('ol'); + $ol->attr['compact'] = 'Bool#compact'; + $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); + $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; + + $p = $this->addBlankElement('p'); + $p->attr['align'] = $align; + + $pre = $this->addBlankElement('pre'); + $pre->attr['width'] = 'Number'; + + // script omitted + + $table = $this->addBlankElement('table'); + $table->attr['align'] = 'Enum#left,center,right'; + $table->attr['bgcolor'] = 'Color'; + + $tr = $this->addBlankElement('tr'); + $tr->attr['bgcolor'] = 'Color'; + + $th = $this->addBlankElement('th'); + $th->attr['bgcolor'] = 'Color'; + $th->attr['height'] = 'Length'; + $th->attr['nowrap'] = 'Bool#nowrap'; + $th->attr['width'] = 'Length'; + + $td = $this->addBlankElement('td'); + $td->attr['bgcolor'] = 'Color'; + $td->attr['height'] = 'Length'; + $td->attr['nowrap'] = 'Bool#nowrap'; + $td->attr['width'] = 'Length'; + + $ul = $this->addBlankElement('ul'); + $ul->attr['compact'] = 'Bool#compact'; + $ul->attr['type'] = 'Enum#square,disc,circle'; + + // "safe" modifications to "unsafe" elements + // WARNING: If you want to add support for an unsafe, legacy + // attribute, make a new TrustedLegacy module with the trusted + // bit set appropriately + + $form = $this->addBlankElement('form'); + $form->content_model = 'Flow | #PCDATA'; + $form->content_model_type = 'optional'; + $form->attr['target'] = 'FrameTarget'; + + $input = $this->addBlankElement('input'); + $input->attr['align'] = 'IAlign'; + + $legend = $this->addBlankElement('legend'); + $legend->attr['align'] = 'LAlign'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php new file mode 100644 index 0000000..7a20ff7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php @@ -0,0 +1,51 @@ + 'List'); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + // XXX The wrap attribute is handled by MakeWellFormed. This is all + // quite unsatisfactory, because we generated this + // *specifically* for lists, and now a big chunk of the handling + // is done properly by the List ChildDef. So actually, we just + // want enough information to make autoclosing work properly, + // and then hand off the tricky stuff to the ChildDef. + $ol->wrap = 'li'; + $ul->wrap = 'li'; + $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); + + $this->addElement('li', false, 'Flow', 'Common'); + + $this->addElement('dd', false, 'Flow', 'Common'); + $this->addElement('dt', false, 'Inline', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php new file mode 100644 index 0000000..60c0545 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php @@ -0,0 +1,26 @@ +addBlankElement($name); + $element->attr['name'] = 'CDATA'; + if (!$config->get('HTML.Attr.Name.UseCDATA')) { + $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php new file mode 100644 index 0000000..dc9410a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php @@ -0,0 +1,25 @@ +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php new file mode 100644 index 0000000..da72225 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php @@ -0,0 +1,20 @@ + array( + 'lang' => 'LanguageCode', + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php new file mode 100644 index 0000000..2f9efc5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php @@ -0,0 +1,62 @@ + to cater to legacy browsers: this + * module does not allow this sort of behavior + */ +class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule +{ + /** + * @type string + */ + public $name = 'Object'; + + /** + * @type bool + */ + public $safe = false; + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $this->addElement( + 'object', + 'Inline', + 'Optional: #PCDATA | Flow | param', + 'Common', + array( + 'archive' => 'URI', + 'classid' => 'URI', + 'codebase' => 'URI', + 'codetype' => 'Text', + 'data' => 'URI', + 'declare' => 'Bool#declare', + 'height' => 'Length', + 'name' => 'CDATA', + 'standby' => 'Text', + 'tabindex' => 'Number', + 'type' => 'ContentType', + 'width' => 'Length' + ) + ); + + $this->addElement( + 'param', + false, + 'Empty', + null, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'type' => 'Text', + 'value' => 'Text', + 'valuetype' => 'Enum#data,ref,object' + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php new file mode 100644 index 0000000..6458ce9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php @@ -0,0 +1,42 @@ +addElement('hr', 'Block', 'Empty', 'Common'); + $this->addElement('sub', 'Inline', 'Inline', 'Common'); + $this->addElement('sup', 'Inline', 'Inline', 'Common'); + $b = $this->addElement('b', 'Inline', 'Inline', 'Common'); + $b->formatting = true; + $big = $this->addElement('big', 'Inline', 'Inline', 'Common'); + $big->formatting = true; + $i = $this->addElement('i', 'Inline', 'Inline', 'Common'); + $i->formatting = true; + $small = $this->addElement('small', 'Inline', 'Inline', 'Common'); + $small->formatting = true; + $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common'); + $tt->formatting = true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php new file mode 100644 index 0000000..5ee3c8e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php @@ -0,0 +1,40 @@ +addElement( + 'marquee', + 'Inline', + 'Flow', + 'Common', + array( + 'direction' => 'Enum#left,right,up,down', + 'behavior' => 'Enum#alternate', + 'width' => 'Length', + 'height' => 'Length', + 'scrolldelay' => 'Number', + 'scrollamount' => 'Number', + 'loop' => 'Number', + 'bgcolor' => 'Color', + 'hspace' => 'Pixels', + 'vspace' => 'Pixels', + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php new file mode 100644 index 0000000..d1afde0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php @@ -0,0 +1,36 @@ +addElement( + 'ruby', + 'Inline', + 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', + 'Common' + ); + $this->addElement('rbc', false, 'Required: rb', 'Common'); + $this->addElement('rtc', false, 'Required: rt', 'Common'); + $rb = $this->addElement('rb', false, 'Inline', 'Common'); + $rb->excludes = array('ruby' => true); + $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); + $rt->excludes = array('ruby' => true); + $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php new file mode 100644 index 0000000..04e6689 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php @@ -0,0 +1,40 @@ +get('HTML.MaxImgLength'); + $embed = $this->addElement( + 'embed', + 'Inline', + 'Empty', + 'Common', + array( + 'src*' => 'URI#embedded', + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'allowscriptaccess' => 'Enum#never', + 'allownetworking' => 'Enum#internal', + 'flashvars' => 'Text', + 'wmode' => 'Enum#window,transparent,opaque', + 'name' => 'ID', + ) + ); + $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php new file mode 100644 index 0000000..1297f80 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php @@ -0,0 +1,62 @@ +get('HTML.MaxImgLength'); + $object = $this->addElement( + 'object', + 'Inline', + 'Optional: param | Flow | #PCDATA', + 'Common', + array( + // While technically not required by the spec, we're forcing + // it to this value. + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'data' => 'URI#embedded', + 'codebase' => new HTMLPurifier_AttrDef_Enum( + array( + 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0' + ) + ), + ) + ); + $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); + + $param = $this->addElement( + 'param', + false, + 'Empty', + false, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'value' => 'Text' + ) + ); + $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); + $this->info_injector[] = 'SafeObject'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php new file mode 100644 index 0000000..aea7584 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php @@ -0,0 +1,40 @@ +get('HTML.SafeScripting'); + $script = $this->addElement( + 'script', + 'Inline', + 'Optional:', // Not `Empty` to not allow to autoclose the #i', '', $html); + } + + return $html; + } + + /** + * Takes a string of HTML (fragment or document) and returns the content + * @todo Consider making protected + */ + public function extractBody($html) + { + $matches = array(); + $result = preg_match('|(.*?)]*>(.*)|is', $html, $matches); + if ($result) { + // Make sure it's not in a comment + $comment_start = strrpos($matches[1], ''); + if ($comment_start === false || + ($comment_end !== false && $comment_end > $comment_start)) { + return $matches[2]; + } + } + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php new file mode 100644 index 0000000..de79aaa --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php @@ -0,0 +1,411 @@ +factory = new HTMLPurifier_TokenFactory(); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function tokenizeHTML($html, $config, $context) + { + $html = $this->normalize($html, $config, $context); + + // attempt to armor stray angled brackets that cannot possibly + // form tags and thus are probably being used as emoticons + if ($config->get('Core.AggressivelyFixLt')) { + $html = $this->aggressivelyFixLt($html); + } + + // preprocess html, essential for UTF-8 + $html = $this->wrapHTML($html, $config, $context); + + $doc = new DOMDocument(); + $doc->encoding = 'UTF-8'; // theoretically, the above has this covered + + $options = 0; + if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) { + $options |= LIBXML_PARSEHUGE; + } + if ($config->get('Core.RemoveBlanks') && defined('LIBXML_NOBLANKS')) { + $options |= LIBXML_NOBLANKS; + } + + set_error_handler(array($this, 'muteErrorHandler')); + // loadHTML() fails on PHP 5.3 when second parameter is given + if ($options) { + $doc->loadHTML($html, $options); + } else { + $doc->loadHTML($html); + } + restore_error_handler(); + + $body = $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0); // + + $div = $body->getElementsByTagName('div')->item(0); //
+ $tokens = array(); + $this->tokenizeDOM($div, $tokens, $config); + // If the div has a sibling, that means we tripped across + // a premature
tag. So remove the div we parsed, + // and then tokenize the rest of body. We can't tokenize + // the sibling directly as we'll lose the tags in that case. + if ($div->nextSibling) { + $body->removeChild($div); + $this->tokenizeDOM($body, $tokens, $config); + } + return $tokens; + } + + /** + * Iterative function that tokenizes a node, putting it into an accumulator. + * To iterate is human, to recurse divine - L. Peter Deutsch + * @param DOMNode $node DOMNode to be tokenized. + * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. + */ + protected function tokenizeDOM($node, &$tokens, $config) + { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingNodes = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + $collect = $level > 0 ? true : false; + $needEndingTag = $this->createStartNode($node, $tokens, $collect, $config); + if ($needEndingTag) { + $closingNodes[$level][] = $node; + } + if ($node->childNodes && $node->childNodes->length) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->childNodes as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingNodes[$level])) { + while ($node = array_pop($closingNodes[$level])) { + $this->createEndNode($node, $tokens); + } + } + } while ($level > 0); + } + + /** + * Portably retrieve the tag name of a node; deals with older versions + * of libxml like 2.7.6 + * @param DOMNode $node + */ + protected function getTagName($node) + { + if (isset($node->tagName)) { + return $node->tagName; + } else if (isset($node->nodeName)) { + return $node->nodeName; + } else if (isset($node->localName)) { + return $node->localName; + } + return null; + } + + /** + * Portably retrieve the data of a node; deals with older versions + * of libxml like 2.7.6 + * @param DOMNode $node + */ + protected function getData($node) + { + if (isset($node->data)) { + return $node->data; + } else if (isset($node->nodeValue)) { + return $node->nodeValue; + } else if (isset($node->textContent)) { + return $node->textContent; + } + return null; + } + + + /** + * @param DOMNode $node DOMNode to be tokenized. + * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. + * @param bool $collect Says whether or start and close are collected, set to + * false at first recursion because it's the implicit DIV + * tag you're dealing with. + * @return bool if the token needs an endtoken + * @todo data and tagName properties don't seem to exist in DOMNode? + */ + protected function createStartNode($node, &$tokens, $collect, $config) + { + // intercept non element nodes. WE MUST catch all of them, + // but we're not getting the character reference nodes because + // those should have been preprocessed + if ($node->nodeType === XML_TEXT_NODE) { + $data = $this->getData($node); // Handle variable data property + if ($data !== null) { + $tokens[] = $this->factory->createText($data); + } + return false; + } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) { + // undo libxml's special treatment of )#si', + array($this, 'scriptCallback'), + $html + ); + } + + $html = $this->normalize($html, $config, $context); + + $cursor = 0; // our location in the text + $inside_tag = false; // whether or not we're parsing the inside of a tag + $array = array(); // result array + + // This is also treated to mean maintain *column* numbers too + $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); + + if ($maintain_line_numbers === null) { + // automatically determine line numbering by checking + // if error collection is on + $maintain_line_numbers = $config->get('Core.CollectErrors'); + } + + if ($maintain_line_numbers) { + $current_line = 1; + $current_col = 0; + $length = strlen($html); + } else { + $current_line = false; + $current_col = false; + $length = false; + } + $context->register('CurrentLine', $current_line); + $context->register('CurrentCol', $current_col); + $nl = "\n"; + // how often to manually recalculate. This will ALWAYS be right, + // but it's pretty wasteful. Set to 0 to turn off + $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // for testing synchronization + $loops = 0; + + while (++$loops) { + // $cursor is either at the start of a token, or inside of + // a tag (i.e. there was a < immediately before it), as indicated + // by $inside_tag + + if ($maintain_line_numbers) { + // $rcursor, however, is always at the start of a token. + $rcursor = $cursor - (int)$inside_tag; + + // Column number is cheap, so we calculate it every round. + // We're interested at the *end* of the newline string, so + // we need to add strlen($nl) == 1 to $nl_pos before subtracting it + // from our "rcursor" position. + $nl_pos = strrpos($html, $nl, $rcursor - $length); + $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); + + // recalculate lines + if ($synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0) { // time to synchronize! + $current_line = 1 + substr_count($html, $nl, 0, $cursor); + } + } + + $position_next_lt = strpos($html, '<', $cursor); + $position_next_gt = strpos($html, '>', $cursor); + + // triggers on "asdf" but not "asdf " + // special case to set up context + if ($position_next_lt === $cursor) { + $inside_tag = true; + $cursor++; + } + + if (!$inside_tag && $position_next_lt !== false) { + // We are not inside tag and there still is another tag to parse + $token = new + HTMLPurifier_Token_Text( + $this->parseText( + substr( + $html, + $cursor, + $position_next_lt - $cursor + ), $config + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += substr_count($html, $nl, $cursor, $position_next_lt - $cursor); + } + $array[] = $token; + $cursor = $position_next_lt + 1; + $inside_tag = true; + continue; + } elseif (!$inside_tag) { + // We are not inside tag but there are no more tags + // If we're already at the end, break + if ($cursor === strlen($html)) { + break; + } + // Create Text of rest of string + $token = new + HTMLPurifier_Token_Text( + $this->parseText( + substr( + $html, + $cursor + ), $config + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + $array[] = $token; + break; + } elseif ($inside_tag && $position_next_gt !== false) { + // We are in tag and it is well formed + // Grab the internals of the tag + $strlen_segment = $position_next_gt - $cursor; + + if ($strlen_segment < 1) { + // there's nothing to process! + $token = new HTMLPurifier_Token_Text('<'); + $cursor++; + continue; + } + + $segment = substr($html, $cursor, $strlen_segment); + + if ($segment === false) { + // somehow, we attempted to access beyond the end of + // the string, defense-in-depth, reported by Nate Abele + break; + } + + // Check if it's a comment + if (substr($segment, 0, 3) === '!--') { + // re-determine segment length, looking for --> + $position_comment_end = strpos($html, '-->', $cursor); + if ($position_comment_end === false) { + // uh oh, we have a comment that extends to + // infinity. Can't be helped: set comment + // end position to end of string + if ($e) { + $e->send(E_WARNING, 'Lexer: Unclosed comment'); + } + $position_comment_end = strlen($html); + $end = true; + } else { + $end = false; + } + $strlen_segment = $position_comment_end - $cursor; + $segment = substr($html, $cursor, $strlen_segment); + $token = new + HTMLPurifier_Token_Comment( + substr( + $segment, + 3, + $strlen_segment - 3 + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += substr_count($html, $nl, $cursor, $strlen_segment); + } + $array[] = $token; + $cursor = $end ? $position_comment_end : $position_comment_end + 3; + $inside_tag = false; + continue; + } + + // Check if it's an end tag + $is_end_tag = (strpos($segment, '/') === 0); + if ($is_end_tag) { + $type = substr($segment, 1); + $token = new HTMLPurifier_Token_End($type); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Check leading character is alnum, if not, we may + // have accidently grabbed an emoticon. Translate into + // text and go our merry way + if (!ctype_alpha($segment[0])) { + // XML: $segment[0] !== '_' && $segment[0] !== ':' + if ($e) { + $e->send(E_NOTICE, 'Lexer: Unescaped lt'); + } + $token = new HTMLPurifier_Token_Text('<'); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + continue; + } + + // Check if it is explicitly self closing, if so, remove + // trailing slash. Remember, we could have a tag like
, so + // any later token processing scripts must convert improperly + // classified EmptyTags from StartTags. + $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); + if ($is_self_closing) { + $strlen_segment--; + $segment = substr($segment, 0, $strlen_segment); + } + + // Check if there are any attributes + $position_first_space = strcspn($segment, $this->_whitespace); + + if ($position_first_space >= $strlen_segment) { + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($segment); + } else { + $token = new HTMLPurifier_Token_Start($segment); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Grab out all the data + $type = substr($segment, 0, $position_first_space); + $attribute_string = + trim( + substr( + $segment, + $position_first_space + ) + ); + if ($attribute_string) { + $attr = $this->parseAttributeString( + $attribute_string, + $config, + $context + ); + } else { + $attr = array(); + } + + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($type, $attr); + } else { + $token = new HTMLPurifier_Token_Start($type, $attr); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $cursor = $position_next_gt + 1; + $inside_tag = false; + continue; + } else { + // inside tag, but there's no ending > sign + if ($e) { + $e->send(E_WARNING, 'Lexer: Missing gt'); + } + $token = new + HTMLPurifier_Token_Text( + '<' . + $this->parseText( + substr($html, $cursor), $config + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + // no cursor scroll? Hmm... + $array[] = $token; + break; + } + break; + } + + $context->destroy('CurrentLine'); + $context->destroy('CurrentCol'); + return $array; + } + + /** + * Takes the inside of an HTML tag and makes an assoc array of attributes. + * + * @param string $string Inside of tag excluding name. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array Assoc array of attributes. + */ + public function parseAttributeString($string, $config, $context) + { + $string = (string)$string; // quick typecast + + if ($string == '') { + return array(); + } // no attributes + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // let's see if we can abort as quickly as possible + // one equal sign, no spaces => one attribute + $num_equal = substr_count($string, '='); + $has_space = strpos($string, ' '); + if ($num_equal === 0 && !$has_space) { + // bool attribute + return array($string => $string); + } elseif ($num_equal === 1 && !$has_space) { + // only one attribute + list($key, $quoted_value) = explode('=', $string); + $quoted_value = trim($quoted_value); + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + return array(); + } + if (!$quoted_value) { + return array($key => ''); + } + $first_char = @$quoted_value[0]; + $last_char = @$quoted_value[strlen($quoted_value) - 1]; + + $same_quote = ($first_char == $last_char); + $open_quote = ($first_char == '"' || $first_char == "'"); + + if ($same_quote && $open_quote) { + // well behaved + $value = substr($quoted_value, 1, strlen($quoted_value) - 2); + } else { + // not well behaved + if ($open_quote) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing end quote'); + } + $value = substr($quoted_value, 1); + } else { + $value = $quoted_value; + } + } + if ($value === false) { + $value = ''; + } + return array($key => $this->parseAttr($value, $config)); + } + + // setup loop environment + $array = array(); // return assoc array of attributes + $cursor = 0; // current position in string (moves forward) + $size = strlen($string); // size of the string (stays the same) + + // if we have unquoted attributes, the parser expects a terminating + // space, so let's guarantee that there's always a terminating space. + $string .= ' '; + + $old_cursor = -1; + while ($cursor < $size) { + if ($old_cursor >= $cursor) { + throw new Exception("Infinite loop detected"); + } + $old_cursor = $cursor; + + $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); + // grab the key + + $key_begin = $cursor; //we're currently at the start of the key + + // scroll past all characters that are the key (not whitespace or =) + $cursor += strcspn($string, $this->_whitespace . '=', $cursor); + + $key_end = $cursor; // now at the end of the key + + $key = substr($string, $key_begin, $key_end - $key_begin); + + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop + continue; // empty key + } + + // scroll past all whitespace + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor >= $size) { + $array[$key] = $key; + break; + } + + // if the next character is an equal sign, we've got a regular + // pair, otherwise, it's a bool attribute + $first_char = @$string[$cursor]; + + if ($first_char == '=') { + // key="value" + + $cursor++; + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor === false) { + $array[$key] = ''; + break; + } + + // we might be in front of a quote right now + + $char = @$string[$cursor]; + + if ($char == '"' || $char == "'") { + // it's quoted, end bound is $char + $cursor++; + $value_begin = $cursor; + $cursor = strpos($string, $char, $cursor); + $value_end = $cursor; + } else { + // it's not quoted, end bound is whitespace + $value_begin = $cursor; + $cursor += strcspn($string, $this->_whitespace, $cursor); + $value_end = $cursor; + } + + // we reached a premature end + if ($cursor === false) { + $cursor = $size; + $value_end = $cursor; + } + + $value = substr($string, $value_begin, $value_end - $value_begin); + if ($value === false) { + $value = ''; + } + $array[$key] = $this->parseAttr($value, $config); + $cursor++; + } else { + // boolattr + if ($key !== '') { + $array[$key] = $key; + } else { + // purely theoretical + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + } + } + } + return $array; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php new file mode 100644 index 0000000..9390279 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php @@ -0,0 +1,4788 @@ +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */); + try { + $parser = new HTML5($new_html); + $doc = $parser->save(); + } catch (DOMException $e) { + // Uh oh, it failed. Punt to DirectLex. + $lexer = new HTMLPurifier_Lexer_DirectLex(); + $context->register('PH5PError', $e); // save the error, so we can detect it + return $lexer->tokenizeHTML($html, $config, $context); // use original HTML + } + $tokens = array(); + $this->tokenizeDOM( + $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0) // + , + $tokens, $config + ); + return $tokens; + } +} + +/* + +Copyright 2007 Jeroen van der Meer + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +class HTML5 +{ + private $data; + private $char; + private $EOF; + private $state; + private $tree; + private $token; + private $content_model; + private $escape = false; + private $entities = array( + 'AElig;', + 'AElig', + 'AMP;', + 'AMP', + 'Aacute;', + 'Aacute', + 'Acirc;', + 'Acirc', + 'Agrave;', + 'Agrave', + 'Alpha;', + 'Aring;', + 'Aring', + 'Atilde;', + 'Atilde', + 'Auml;', + 'Auml', + 'Beta;', + 'COPY;', + 'COPY', + 'Ccedil;', + 'Ccedil', + 'Chi;', + 'Dagger;', + 'Delta;', + 'ETH;', + 'ETH', + 'Eacute;', + 'Eacute', + 'Ecirc;', + 'Ecirc', + 'Egrave;', + 'Egrave', + 'Epsilon;', + 'Eta;', + 'Euml;', + 'Euml', + 'GT;', + 'GT', + 'Gamma;', + 'Iacute;', + 'Iacute', + 'Icirc;', + 'Icirc', + 'Igrave;', + 'Igrave', + 'Iota;', + 'Iuml;', + 'Iuml', + 'Kappa;', + 'LT;', + 'LT', + 'Lambda;', + 'Mu;', + 'Ntilde;', + 'Ntilde', + 'Nu;', + 'OElig;', + 'Oacute;', + 'Oacute', + 'Ocirc;', + 'Ocirc', + 'Ograve;', + 'Ograve', + 'Omega;', + 'Omicron;', + 'Oslash;', + 'Oslash', + 'Otilde;', + 'Otilde', + 'Ouml;', + 'Ouml', + 'Phi;', + 'Pi;', + 'Prime;', + 'Psi;', + 'QUOT;', + 'QUOT', + 'REG;', + 'REG', + 'Rho;', + 'Scaron;', + 'Sigma;', + 'THORN;', + 'THORN', + 'TRADE;', + 'Tau;', + 'Theta;', + 'Uacute;', + 'Uacute', + 'Ucirc;', + 'Ucirc', + 'Ugrave;', + 'Ugrave', + 'Upsilon;', + 'Uuml;', + 'Uuml', + 'Xi;', + 'Yacute;', + 'Yacute', + 'Yuml;', + 'Zeta;', + 'aacute;', + 'aacute', + 'acirc;', + 'acirc', + 'acute;', + 'acute', + 'aelig;', + 'aelig', + 'agrave;', + 'agrave', + 'alefsym;', + 'alpha;', + 'amp;', + 'amp', + 'and;', + 'ang;', + 'apos;', + 'aring;', + 'aring', + 'asymp;', + 'atilde;', + 'atilde', + 'auml;', + 'auml', + 'bdquo;', + 'beta;', + 'brvbar;', + 'brvbar', + 'bull;', + 'cap;', + 'ccedil;', + 'ccedil', + 'cedil;', + 'cedil', + 'cent;', + 'cent', + 'chi;', + 'circ;', + 'clubs;', + 'cong;', + 'copy;', + 'copy', + 'crarr;', + 'cup;', + 'curren;', + 'curren', + 'dArr;', + 'dagger;', + 'darr;', + 'deg;', + 'deg', + 'delta;', + 'diams;', + 'divide;', + 'divide', + 'eacute;', + 'eacute', + 'ecirc;', + 'ecirc', + 'egrave;', + 'egrave', + 'empty;', + 'emsp;', + 'ensp;', + 'epsilon;', + 'equiv;', + 'eta;', + 'eth;', + 'eth', + 'euml;', + 'euml', + 'euro;', + 'exist;', + 'fnof;', + 'forall;', + 'frac12;', + 'frac12', + 'frac14;', + 'frac14', + 'frac34;', + 'frac34', + 'frasl;', + 'gamma;', + 'ge;', + 'gt;', + 'gt', + 'hArr;', + 'harr;', + 'hearts;', + 'hellip;', + 'iacute;', + 'iacute', + 'icirc;', + 'icirc', + 'iexcl;', + 'iexcl', + 'igrave;', + 'igrave', + 'image;', + 'infin;', + 'int;', + 'iota;', + 'iquest;', + 'iquest', + 'isin;', + 'iuml;', + 'iuml', + 'kappa;', + 'lArr;', + 'lambda;', + 'lang;', + 'laquo;', + 'laquo', + 'larr;', + 'lceil;', + 'ldquo;', + 'le;', + 'lfloor;', + 'lowast;', + 'loz;', + 'lrm;', + 'lsaquo;', + 'lsquo;', + 'lt;', + 'lt', + 'macr;', + 'macr', + 'mdash;', + 'micro;', + 'micro', + 'middot;', + 'middot', + 'minus;', + 'mu;', + 'nabla;', + 'nbsp;', + 'nbsp', + 'ndash;', + 'ne;', + 'ni;', + 'not;', + 'not', + 'notin;', + 'nsub;', + 'ntilde;', + 'ntilde', + 'nu;', + 'oacute;', + 'oacute', + 'ocirc;', + 'ocirc', + 'oelig;', + 'ograve;', + 'ograve', + 'oline;', + 'omega;', + 'omicron;', + 'oplus;', + 'or;', + 'ordf;', + 'ordf', + 'ordm;', + 'ordm', + 'oslash;', + 'oslash', + 'otilde;', + 'otilde', + 'otimes;', + 'ouml;', + 'ouml', + 'para;', + 'para', + 'part;', + 'permil;', + 'perp;', + 'phi;', + 'pi;', + 'piv;', + 'plusmn;', + 'plusmn', + 'pound;', + 'pound', + 'prime;', + 'prod;', + 'prop;', + 'psi;', + 'quot;', + 'quot', + 'rArr;', + 'radic;', + 'rang;', + 'raquo;', + 'raquo', + 'rarr;', + 'rceil;', + 'rdquo;', + 'real;', + 'reg;', + 'reg', + 'rfloor;', + 'rho;', + 'rlm;', + 'rsaquo;', + 'rsquo;', + 'sbquo;', + 'scaron;', + 'sdot;', + 'sect;', + 'sect', + 'shy;', + 'shy', + 'sigma;', + 'sigmaf;', + 'sim;', + 'spades;', + 'sub;', + 'sube;', + 'sum;', + 'sup1;', + 'sup1', + 'sup2;', + 'sup2', + 'sup3;', + 'sup3', + 'sup;', + 'supe;', + 'szlig;', + 'szlig', + 'tau;', + 'there4;', + 'theta;', + 'thetasym;', + 'thinsp;', + 'thorn;', + 'thorn', + 'tilde;', + 'times;', + 'times', + 'trade;', + 'uArr;', + 'uacute;', + 'uacute', + 'uarr;', + 'ucirc;', + 'ucirc', + 'ugrave;', + 'ugrave', + 'uml;', + 'uml', + 'upsih;', + 'upsilon;', + 'uuml;', + 'uuml', + 'weierp;', + 'xi;', + 'yacute;', + 'yacute', + 'yen;', + 'yen', + 'yuml;', + 'yuml', + 'zeta;', + 'zwj;', + 'zwnj;' + ); + + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTR = 4; + const EOF = 5; + + public function __construct($data) + { + $this->data = $data; + $this->char = -1; + $this->EOF = strlen($data); + $this->tree = new HTML5TreeConstructer; + $this->content_model = self::PCDATA; + + $this->state = 'data'; + + while ($this->state !== null) { + $this->{$this->state . 'State'}(); + } + } + + public function save() + { + return $this->tree->save(); + } + + private function char() + { + return ($this->char < $this->EOF) + ? $this->data[$this->char] + : false; + } + + private function character($s, $l = 0) + { + if ($s + $l < $this->EOF) { + if ($l === 0) { + return $this->data[$s]; + } else { + return substr($this->data, $s, $l); + } + } + } + + private function characters($char_class, $start) + { + return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); + } + + private function dataState() + { + // Consume the next input character + $this->char++; + $char = $this->char(); + + if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states: switch to the entity data state. Otherwise: treat it as per + the "anything else" entry below. */ + $this->state = 'entityData'; + + } elseif ($char === '-') { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is false, and there are at + least three characters before this one in the input stream, and the + last four characters in the input stream, including this one, are + U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, + and U+002D HYPHEN-MINUS (""), + set the escape flag to false. */ + if (($this->content_model === self::RCDATA || + $this->content_model === self::CDATA) && $this->escape === true && + $this->character($this->char, 3) === '-->' + ) { + $this->escape = false; + } + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + } elseif ($this->char === $this->EOF) { + /* EOF + Emit an end-of-file token. */ + $this->EOF(); + + } elseif ($this->content_model === self::PLAINTEXT) { + /* When the content model flag is set to the PLAINTEXT state + THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of + the text and emit it as a character token. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => substr($this->data, $this->char) + ) + ); + + $this->EOF(); + + } else { + /* Anything else + THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + $len = strcspn($this->data, '<&', $this->char); + $char = substr($this->data, $this->char, $len); + $this->char += $len - 1; + + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + $this->state = 'data'; + } + } + + private function entityDataState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + // Finally, switch to the data state. + $this->state = 'data'; + } + + private function tagOpenState() + { + switch ($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* If the next input character is a U+002F SOLIDUS (/) character, + consume it and switch to the close tag open state. If the next + input character is not a U+002F SOLIDUS (/) character, emit a + U+003C LESS-THAN SIGN character token and switch to the data + state to process the next input character. */ + if ($this->character($this->char + 1) === '/') { + $this->char++; + $this->state = 'closeTagOpen'; + + } else { + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->state = 'data'; + } + break; + + case self::PCDATA: + // If the content model flag is set to the PCDATA state + // Consume the next input character: + $this->char++; + $char = $this->char(); + + if ($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $this->state = 'markupDeclarationOpen'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $this->state = 'closeTagOpen'; + + } elseif (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<>' + ) + ); + + $this->state = 'data'; + + } elseif ($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->char--; + $this->state = 'data'; + } + break; + } + } + + private function closeTagOpenState() + { + $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); + $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; + + if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && + (!$the_same || ($the_same && (!preg_match( + '/[\t\n\x0b\x0c >\/]/', + $this->character($this->char + 1 + strlen($next_node)) + ) || $this->EOF === $this->char))) + ) { + /* If the content model flag is set to the RCDATA or CDATA states then + examine the next few characters. If they do not match the tag name of + the last start tag token emitted (case insensitively), or if they do but + they are not immediately followed by one of the following characters: + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000B LINE TABULATION + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character + token, a U+002F SOLIDUS character token, and switch to the data state + to process the next input character. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'state = 'data'; + + } else { + /* Otherwise, if the content model flag is set to the PCDATA state, + or if the next few characters do match that tag name, consume the + next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'char--; + $this->state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + } + } + } + + private function tagNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $this->token['name'] .= strtolower($char); + $this->state = 'tagName'; + } + } + + private function beforeAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Stay in the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function attributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char); + + $this->state = 'attributeName'; + } + } + + private function afterAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the + before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function beforeAttributeValueState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $this->state = 'attributeValueDoubleQuoted'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->char--; + $this->state = 'attributeValueUnquoted'; + + } elseif ($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $this->state = 'attributeValueSingleQuoted'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function attributeValueDoubleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('double'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueDoubleQuoted'; + } + } + + private function attributeValueSingleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '\'') { + /* U+0022 QUOTATION MARK (') + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('single'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueSingleQuoted'; + } + } + + private function attributeValueUnquotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState(); + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function entityInAttributeValueState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, append a U+0026 AMPERSAND character to the + // current attribute's value. Otherwise, emit the character token that + // was returned. + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() + { + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $data = $this->characters('^>', $this->char); + $this->emitToken( + array( + 'data' => $data, + 'type' => self::COMMENT + ) + ); + + $this->char += strlen($data); + + /* Switch to the data state. */ + $this->state = 'data'; + + /* If the end of the file was reached, reconsume the EOF character. */ + if ($this->char === $this->EOF) { + $this->char = $this->EOF - 1; + } + } + + private function markupDeclarationOpenState() + { + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if ($this->character($this->char + 1, 2) === '--') { + $this->char += 2; + $this->state = 'comment'; + $this->token = array( + 'data' => null, + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven characters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { + $this->char += 7; + $this->state = 'doctype'; + + /* Otherwise, it is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->char++; + $this->state = 'bogusComment'; + } + } + + private function commentState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment dash state */ + $this->state = 'commentDash'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append the input character to the comment token's data. Stay in + the comment state. */ + $this->token['data'] .= $char; + } + } + + private function commentDashState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment end state */ + $this->state = 'commentEnd'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-' . $char; + $this->state = 'comment'; + } + } + + private function commentEndState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '-') { + $this->token['data'] .= '-'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['data'] .= '--' . $char; + $this->state = 'comment'; + } + } + + private function doctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'beforeDoctypeName'; + + } else { + $this->char--; + $this->state = 'beforeDoctypeName'; + } + } + + private function beforeDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the before DOCTYPE name state. + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token = array( + 'name' => strtoupper($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + + } elseif ($char === '>') { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->char--; + $this->state = 'data'; + + } else { + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + } + } + + private function doctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'AfterDoctypeName'; + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token['name'] .= strtoupper($char); + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['name'] .= $char; + } + + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + } + + private function afterDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the DOCTYPE name state. + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['error'] = true; + $this->state = 'bogusDoctype'; + } + } + + private function bogusDoctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + // Stay in the bogus DOCTYPE state. + } + } + + private function entity() + { + $start = $this->char; + + // This section defines how to consume an entity. This definition is + // used when parsing entities in text and in attributes. + + // The behaviour depends on the identity of the next character (the + // one immediately after the U+0026 AMPERSAND character): + + switch ($this->character($this->char + 1)) { + // U+0023 NUMBER SIGN (#) + case '#': + + // The behaviour further depends on the character after the + // U+0023 NUMBER SIGN: + switch ($this->character($this->char + 1)) { + // U+0078 LATIN SMALL LETTER X + // U+0058 LATIN CAPITAL LETTER X + case 'x': + case 'X': + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + // A, through to U+0046 LATIN CAPITAL LETTER F (in other + // words, 0-9, A-F, a-f). + $char = 1; + $char_class = '0-9A-Fa-f'; + break; + + // Anything else + default: + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE (i.e. just 0-9). + $char = 0; + $char_class = '0-9'; + break; + } + + // Consume as many characters as match the range of characters + // given above. + $this->char++; + $e_name = $this->characters($char_class, $this->char + $char + 1); + $entity = $this->character($start, $this->char); + $cond = strlen($e_name) > 0; + + // The rest of the parsing happens below. + break; + + // Anything else + default: + // Consume the maximum number of characters possible, with the + // consumed characters case-sensitively matching one of the + // identifiers in the first column of the entities table. + + $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); + $len = strlen($e_name); + + for ($c = 1; $c <= $len; $c++) { + $id = substr($e_name, 0, $c); + $this->char++; + + if (in_array($id, $this->entities)) { + if ($e_name[$c - 1] !== ';') { + if ($c < $len && $e_name[$c] == ';') { + $this->char++; // consume extra semicolon + } + } + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens below. + break; + } + + if (!$cond) { + // If no match can be made, then this is a parse error. No + // characters are consumed, and nothing is returned. + $this->char = $start; + return false; + } + + // Return a character token for the character corresponding to the + // entity name (as given by the second column of the entities table). + return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8'); + } + + private function emitToken($token) + { + $emit = $this->tree->emitToken($token); + + if (is_int($emit)) { + $this->content_model = $emit; + + } elseif ($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } + + private function EOF() + { + $this->state = null; + $this->tree->emitToken( + array( + 'type' => self::EOF + ) + ); + } +} + +class HTML5TreeConstructer +{ + public $stack = array(); + + private $phase; + private $mode; + private $dom; + private $foster_parent = null; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); + private $formatting = array( + 'a', + 'b', + 'big', + 'em', + 'font', + 'i', + 'nobr', + 's', + 'small', + 'strike', + 'strong', + 'tt', + 'u' + ); + private $special = array( + 'address', + 'area', + 'base', + 'basefont', + 'bgsound', + 'blockquote', + 'body', + 'br', + 'center', + 'col', + 'colgroup', + 'dd', + 'dir', + 'div', + 'dl', + 'dt', + 'embed', + 'fieldset', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'hr', + 'iframe', + 'image', + 'img', + 'input', + 'isindex', + 'li', + 'link', + 'listing', + 'menu', + 'meta', + 'noembed', + 'noframes', + 'noscript', + 'ol', + 'optgroup', + 'option', + 'p', + 'param', + 'plaintext', + 'pre', + 'script', + 'select', + 'spacer', + 'style', + 'tbody', + 'textarea', + 'tfoot', + 'thead', + 'title', + 'tr', + 'ul', + 'wbr' + ); + + // The different phases. + const INIT_PHASE = 0; + const ROOT_PHASE = 1; + const MAIN_PHASE = 2; + const END_PHASE = 3; + + // The different insertion modes for the main phase. + const BEFOR_HEAD = 0; + const IN_HEAD = 1; + const AFTER_HEAD = 2; + const IN_BODY = 3; + const IN_TABLE = 4; + const IN_CAPTION = 5; + const IN_CGROUP = 6; + const IN_TBODY = 7; + const IN_ROW = 8; + const IN_CELL = 9; + const IN_SELECT = 10; + const AFTER_BODY = 11; + const IN_FRAME = 12; + const AFTR_FRAME = 13; + + // The different types of elements. + const SPECIAL = 0; + const SCOPING = 1; + const FORMATTING = 2; + const PHRASING = 3; + + const MARKER = 0; + + public function __construct() + { + $this->phase = self::INIT_PHASE; + $this->mode = self::BEFOR_HEAD; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token) + { + switch ($this->phase) { + case self::INIT_PHASE: + return $this->initPhase($token); + break; + case self::ROOT_PHASE: + return $this->rootElementPhase($token); + break; + case self::MAIN_PHASE: + return $this->mainPhase($token); + break; + case self::END_PHASE : + return $this->trailingEndPhase($token); + break; + } + } + + private function initPhase($token) + { + /* Initially, the tree construction stage must handle each token + emitted from the tokenisation stage as follows: */ + + /* A DOCTYPE token that is marked as being in error + A comment token + A start tag token + An end tag token + A character token that is not one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE + An end-of-file token */ + if ((isset($token['error']) && $token['error']) || + $token['type'] === HTML5::COMMENT || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF || + ($token['type'] === HTML5::CHARACTR && isset($token['data']) && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) + ) { + /* This specification does not define how to handle this case. In + particular, user agents may ignore the entirety of this specification + altogether for such documents, and instead invoke special parse modes + with a greater emphasis on backwards compatibility. */ + + $this->phase = self::ROOT_PHASE; + return $this->rootElementPhase($token); + + /* A DOCTYPE token marked as being correct */ + } elseif (isset($token['error']) && !$token['error']) { + /* Append a DocumentType node to the Document node, with the name + attribute set to the name given in the DOCTYPE token (which will be + "HTML"), and the other attributes specific to DocumentType objects + set to null, empty lists, or the empty string as appropriate. */ + $doctype = new DOMDocumentType(null, null, 'HTML'); + + /* Then, switch to the root element phase of the tree construction + stage. */ + $this->phase = self::ROOT_PHASE; + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif (isset($token['data']) && preg_match( + '/^[\t\n\x0b\x0c ]+$/', + $token['data'] + ) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + } + } + + private function rootElementPhase($token) + { + /* After the initial phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED + (FF), or U+0020 SPACE + A start tag token + An end tag token + An end-of-file token */ + } elseif (($token['type'] === HTML5::CHARACTR && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF + ) { + /* Create an HTMLElement node with the tag name html, in the HTML + namespace. Append it to the Document object. Switch to the main + phase and reprocess the current token. */ + $html = $this->dom->createElement('html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + } + } + + private function mainPhase($token) + { + /* Tokens in the main phase must be handled as follows: */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A start tag token with the tag name "html" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { + /* If this start tag token was not the first start tag token, then + it is a parse error. */ + + /* For each attribute on the token, check to see if the attribute + is already present on the top element of the stack of open elements. + If it is not, add the attribute and its corresponding value to that + element. */ + foreach ($token['attr'] as $attr) { + if (!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Anything else. */ + } else { + /* Depends on the insertion mode: */ + switch ($this->mode) { + case self::BEFOR_HEAD: + return $this->beforeHead($token); + break; + case self::IN_HEAD: + return $this->inHead($token); + break; + case self::AFTER_HEAD: + return $this->afterHead($token); + break; + case self::IN_BODY: + return $this->inBody($token); + break; + case self::IN_TABLE: + return $this->inTable($token); + break; + case self::IN_CAPTION: + return $this->inCaption($token); + break; + case self::IN_CGROUP: + return $this->inColumnGroup($token); + break; + case self::IN_TBODY: + return $this->inTableBody($token); + break; + case self::IN_ROW: + return $this->inRow($token); + break; + case self::IN_CELL: + return $this->inCell($token); + break; + case self::IN_SELECT: + return $this->inSelect($token); + break; + case self::AFTER_BODY: + return $this->afterBody($token); + break; + case self::IN_FRAME: + return $this->inFrameset($token); + break; + case self::AFTR_FRAME: + return $this->afterFrameset($token); + break; + case self::END_PHASE: + return $this->trailingEndPhase($token); + break; + } + } + } + + private function beforeHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "head" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { + /* Create an element for the token, append the new element to the + current node and push it onto the stack of open elements. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title". Or an end tag with the tag name "html". + Or a character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or any other start tag token */ + } elseif ($token['type'] === HTML5::STARTTAG || + ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || + ($token['type'] === HTML5::CHARACTR && !preg_match( + '/^[\t\n\x0b\x0c ]$/', + $token['data'] + )) + ) { + /* Act as if a start tag token with the tag name "head" and no + attributes had been seen, then reprocess the current token. */ + $this->beforeHead( + array( + 'name' => 'head', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inHead($token); + + /* Any other end tag */ + } elseif ($token['type'] === HTML5::ENDTAG) { + /* Parse error. Ignore the token. */ + } + } + + private function inHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. + + THIS DIFFERS FROM THE SPEC: If the current node is either a title, style + or script element, append the character to the current node regardless + of its content. */ + if (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( + $token['type'] === HTML5::CHARACTR && in_array( + end($this->stack)->nodeName, + array('title', 'style', 'script') + )) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('title', 'style', 'script')) + ) { + array_pop($this->stack); + return HTML5::PCDATA; + + /* A start tag with the tag name "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $element = $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the RCDATA state. */ + return HTML5::RCDATA; + + /* A start tag with the tag name "style" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "script" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { + /* Create an element for the token. */ + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "base", "link", or "meta" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta') + ) + ) { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + array_pop($this->stack); + + } else { + $this->insertElement($token); + } + + /* An end tag with the tag name "head" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { + /* If the current node is a head element, pop the current node off + the stack of open elements. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + array_pop($this->stack); + + /* Otherwise, this is a parse error. */ + } else { + // k + } + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + /* A start tag with the tag name "head" or an end tag except "html". */ + } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* If the current node is a head element, act as if an end tag + token with the tag name "head" had been seen. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + $this->inHead( + array( + 'name' => 'head', + 'type' => HTML5::ENDTAG + ) + ); + + /* Otherwise, change the insertion mode to "after head". */ + } else { + $this->mode = self::AFTER_HEAD; + } + + /* Then, reprocess the current token. */ + return $this->afterHead($token); + } + } + + private function afterHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "body" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { + /* Insert a body element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAME; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta', 'script', 'style', 'title') + ) + ) { + /* Parse error. Switch the insertion mode back to "in head" and + reprocess the token. */ + $this->mode = self::IN_HEAD; + return $this->inHead($token); + + /* Anything else */ + } else { + /* Act as if a start tag token with the tag name "body" and no + attributes had been seen, and then reprocess the current token. */ + $this->afterHead( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inBody($token); + } + } + + private function inBody($token) + { + /* Handle the token as follows: */ + + switch ($token['type']) { + /* A character token */ + case HTML5::CHARACTR: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + break; + + /* A comment token */ + case HTML5::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5::STARTTAG: + switch ($token['name']) { + /* A start tag token whose tag name is one of: "script", + "style" */ + case 'script': + case 'style': + /* Process the token as if the insertion mode had been "in + head". */ + return $this->inHead($token); + break; + + /* A start tag token whose tag name is one of: "base", "link", + "meta", "title" */ + case 'base': + case 'link': + case 'meta': + case 'title': + /* Parse error. Process the token as if the insertion mode + had been "in head". */ + return $this->inHead($token); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (innerHTML case) */ + if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach ($token['attr'] as $attr) { + if (!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + /* A start tag whose tag name is one of: "address", + "blockquote", "center", "dir", "div", "dl", "fieldset", + "listing", "menu", "ol", "p", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'p': + case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if ($this->form_pointer !== null) { + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + /* A start tag whose tag name is "li", "dd" or "dt" */ + case 'li': + case 'dd': + case 'dt': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + $stack_length = count($this->stack) - 1; + + for ($n = $stack_length; 0 <= $n; $n--) { + /* 1. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node->tagName); + + /* 2. If node is an li, dd or dt element, then pop all + the nodes from the current node up to node, including + node, then stop this algorithm. */ + if ($token['name'] === $node->tagName || ($token['name'] !== 'li' + && ($node->tagName === 'dd' || $node->tagName === 'dt')) + ) { + for ($x = $stack_length; $x >= $n; $x--) { + array_pop($this->stack); + } + + break; + } + + /* 3. If node is not in the formatting category, and is + not in the phrasing category, and is not an address or + div element, then stop this algorithm. */ + if ($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div' + ) { + break; + } + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + return HTML5::PLAINTEXT; + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + this is a parse error; pop elements from the stack until an + element with one of those tag names has been popped from the + stack. */ + while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { + array_pop($this->stack); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for ($n = $leng - 1; $n >= 0; $n--) { + if ($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$n]->nodeName === 'a') { + $this->emitToken( + array( + 'name' => 'a', + 'type' => HTML5::ENDTAG + ) + ); + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag whose tag name is one of: "b", "big", "em", "font", + "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) */ + if ($this->elementInScope('button')) { + $this->inBody( + array( + 'name' => 'button', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is one of: "marquee", "object" */ + case 'marquee': + case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is "xmp" */ + case 'xmp': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Switch the content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'img': + case 'param': + case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + return $this->inBody($token); + break; + + /* A start tag whose tag name is "input" */ + case 'input': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an input element for the token. */ + $element = $this->insertElement($token, false); + + /* If the form element pointer is not null, then associate the + input element with the form element pointed to by the form + element pointer. */ + $this->form_pointer !== null + ? $this->form_pointer->appendChild($element) + : end($this->stack)->appendChild($element); + + /* Pop that input element off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + // w/e + + /* If the form element pointer is not null, + then ignore the token. */ + if ($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a stream of character tokens had been seen. */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = $token['attr']; + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->inBody( + array( + 'name' => 'input', + 'type' => HTML5::STARTTAG, + 'attr' => $attr + ) + ); + + /* Act as if a stream of character tokens had been seen + (see below for what they should say). */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'form', + 'type' => HTML5::ENDTAG + ) + ); + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the + RCDATA state. */ + return HTML5::RCDATA; + break; + + /* A start tag whose tag name is one of: "iframe", "noembed", + "noframes" */ + case 'iframe': + case 'noembed': + case 'noframes': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "select" */ + case 'select': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in select". */ + $this->mode = self::IN_SELECT; + break; + + /* A start or end tag whose tag name is one of: "caption", "col", + "colgroup", "frame", "frameset", "head", "option", "optgroup", + "tbody", "td", "tfoot", "th", "thead", "tr". */ + case 'caption': + case 'col': + case 'colgroup': + case 'frame': + case 'frameset': + case 'head': + case 'option': + case 'optgroup': + case 'tbody': + case 'td': + case 'tfoot': + case 'th': + case 'thead': + case 'tr': + // Parse error. Ignore the token. + break; + + /* A start or end tag whose tag name is one of: "event-source", + "section", "nav", "article", "aside", "header", "footer", + "datagrid", "command" */ + case 'event-source': + case 'section': + case 'nav': + case 'article': + case 'aside': + case 'header': + case 'footer': + case 'datagrid': + case 'command': + // Work in progress! + break; + + /* A start tag token not covered by the previous entries */ + default: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + $this->insertElement($token, true, true); + break; + } + break; + + case HTML5::ENDTAG: + switch ($token['name']) { + /* An end tag with the tag name "body" */ + case 'body': + /* If the second element in the stack of open elements is + not a body element, this is a parse error. Ignore the token. + (innerHTML case) */ + if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { + // Ignore. + + /* If the current node is not the body element, then this + is a parse error. */ + } elseif (end($this->stack)->nodeName !== 'body') { + // Parse error. + } + + /* Change the insertion mode to "after body". */ + $this->mode = self::AFTER_BODY; + break; + + /* An end tag with the tag name "html" */ + case 'html': + /* Act as if an end tag with tag name "body" had been seen, + then, if that token wasn't ignored, reprocess the current + token. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->afterBody($token); + break; + + /* An end tag whose tag name is one of: "address", "blockquote", + "center", "dir", "div", "dl", "fieldset", "listing", "menu", + "ol", "pre", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'pre': + case 'ul': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with + the same tag name as that of the token, then this + is a parse error. */ + // w/e + + /* If the stack of open elements has an element in + scope with the same tag name as that of the token, + then pop elements from this stack until an element + with that tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is "form" */ + case 'form': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + } + + if (end($this->stack)->nodeName !== $token['name']) { + /* Now, if the current node is not an element with the + same tag name as that of the token, then this is a parse + error. */ + // w/e + + } else { + /* Otherwise, if the current node is an element with + the same tag name as that of the token pop that element + from the stack. */ + array_pop($this->stack); + } + + /* In any case, set the form element pointer to null. */ + $this->form_pointer = null; + break; + + /* An end tag whose tag name is "p" */ + case 'p': + /* If the stack of open elements has a p element in scope, + then generate implied end tags, except for p elements. */ + if ($this->elementInScope('p')) { + $this->generateImpliedEndTags(array('p')); + + /* If the current node is not a p element, then this is + a parse error. */ + // k + + /* If the stack of open elements has a p element in + scope, then pop elements from this stack until the stack + no longer has a p element in scope. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->elementInScope('p')) { + array_pop($this->stack); + + } else { + break; + } + } + } + break; + + /* An end tag whose tag name is "dd", "dt", or "li" */ + case 'dd': + case 'dt': + case 'li': + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + generate implied end tags, except for elements with the + same tag name as the token. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(array($token['name'])); + + /* If the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + pop elements from this stack until an element with that + tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + generate implied end tags. */ + if ($this->elementInScope($elements)) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as that of the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has in scope an element + whose tag name is one of "h1", "h2", "h3", "h4", "h5", or + "h6", then pop elements from the stack until an element + with one of those tag names has been popped from the stack. */ + while ($this->elementInScope($elements)) { + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "a", "b", "big", "em", + "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'a': + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* 1. Let the formatting element be the last element in + the list of active formatting elements that: + * is between the end of the list and the last scope + marker in the list, if any, or the start of the list + otherwise, and + * has the same tag name as the token. + */ + while (true) { + for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { + if ($this->a_formatting[$a] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$a]->tagName === $token['name']) { + $formatting_element = $this->a_formatting[$a]; + $in_stack = in_array($formatting_element, $this->stack, true); + $fe_af_pos = $a; + break; + } + } + + /* If there is no such node, or, if that node is + also in the stack of open elements but the element + is not in scope, then this is a parse error. Abort + these steps. The token is ignored. */ + if (!isset($formatting_element) || ($in_stack && + !$this->elementInScope($token['name'])) + ) { + break; + + /* Otherwise, if there is such a node, but that node + is not in the stack of open elements, then this is a + parse error; remove the element from the list, and + abort these steps. */ + } elseif (isset($formatting_element) && !$in_stack) { + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 2. Let the furthest block be the topmost node in the + stack of open elements that is lower in the stack + than the formatting element, and is not an element in + the phrasing or formatting categories. There might + not be one. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $length = count($this->stack); + + for ($s = $fe_s_pos + 1; $s < $length; $s++) { + $category = $this->getElementCategory($this->stack[$s]->nodeName); + + if ($category !== self::PHRASING && $category !== self::FORMATTING) { + $furthest_block = $this->stack[$s]; + } + } + + /* 3. If there is no furthest block, then the UA must + skip the subsequent steps and instead just pop all + the nodes from the bottom of the stack of open + elements, from the current node up to the formatting + element, and remove the formatting element from the + list of active formatting elements. */ + if (!isset($furthest_block)) { + for ($n = $length - 1; $n >= $fe_s_pos; $n--) { + array_pop($this->stack); + } + + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 4. Let the common ancestor be the element + immediately above the formatting element in the stack + of open elements. */ + $common_ancestor = $this->stack[$fe_s_pos - 1]; + + /* 5. If the furthest block has a parent node, then + remove the furthest block from its parent node. */ + if ($furthest_block->parentNode !== null) { + $furthest_block->parentNode->removeChild($furthest_block); + } + + /* 6. Let a bookmark note the position of the + formatting element in the list of active formatting + elements relative to the elements on either side + of it in the list. */ + $bookmark = $fe_af_pos; + + /* 7. Let node and last node be the furthest block. + Follow these steps: */ + $node = $furthest_block; + $last_node = $furthest_block; + + while (true) { + for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { + /* 7.1 Let node be the element immediately + prior to node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 7.2 If node is not in the list of active + formatting elements, then remove node from + the stack of open elements and then go back + to step 1. */ + if (!in_array($node, $this->a_formatting, true)) { + unset($this->stack[$n]); + $this->stack = array_merge($this->stack); + + } else { + break; + } + } + + /* 7.3 Otherwise, if node is the formatting + element, then go to the next step in the overall + algorithm. */ + if ($node === $formatting_element) { + break; + + /* 7.4 Otherwise, if last node is the furthest + block, then move the aforementioned bookmark to + be immediately after the node in the list of + active formatting elements. */ + } elseif ($last_node === $furthest_block) { + $bookmark = array_search($node, $this->a_formatting, true) + 1; + } + + /* 7.5 If node has any children, perform a + shallow clone of node, replace the entry for + node in the list of active formatting elements + with an entry for the clone, replace the entry + for node in the stack of open elements with an + entry for the clone, and let node be the clone. */ + if ($node->hasChildNodes()) { + $clone = $node->cloneNode(); + $s_pos = array_search($node, $this->stack, true); + $a_pos = array_search($node, $this->a_formatting, true); + + $this->stack[$s_pos] = $clone; + $this->a_formatting[$a_pos] = $clone; + $node = $clone; + } + + /* 7.6 Insert last node into node, first removing + it from its previous parent node if any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $node->appendChild($last_node); + + /* 7.7 Let last node be node. */ + $last_node = $node; + } + + /* 8. Insert whatever last node ended up being in + the previous step into the common ancestor node, + first removing it from its previous parent node if + any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $common_ancestor->appendChild($last_node); + + /* 9. Perform a shallow clone of the formatting + element. */ + $clone = $formatting_element->cloneNode(); + + /* 10. Take all of the child nodes of the furthest + block and append them to the clone created in the + last step. */ + while ($furthest_block->hasChildNodes()) { + $child = $furthest_block->firstChild; + $furthest_block->removeChild($child); + $clone->appendChild($child); + } + + /* 11. Append that clone to the furthest block. */ + $furthest_block->appendChild($clone); + + /* 12. Remove the formatting element from the list + of active formatting elements, and insert the clone + into the list of active formatting elements at the + position of the aforementioned bookmark. */ + $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + + $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); + $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); + $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); + + /* 13. Remove the formatting element from the stack + of open elements, and insert the clone into the stack + of open elements immediately after (i.e. in a more + deeply nested position than) the position of the + furthest block in that stack. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $fb_s_pos = array_search($furthest_block, $this->stack, true); + unset($this->stack[$fe_s_pos]); + + $s_part1 = array_slice($this->stack, 0, $fb_s_pos); + $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); + $this->stack = array_merge($s_part1, array($clone), $s_part2); + + /* 14. Jump back to step 1 in this series of steps. */ + unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); + } + break; + + /* An end tag token whose tag name is one of: "button", + "marquee", "object" */ + case 'button': + case 'marquee': + case 'object': + /* If the stack of open elements has an element in scope whose + tag name matches the tag name of the token, then generate implied + tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // k + + /* Now, if the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then pop + elements from the stack until that element has been popped from + the stack, and clear the list of active formatting elements up + to the last marker. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + + $marker = end(array_keys($this->a_formatting, self::MARKER, true)); + + for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { + array_pop($this->a_formatting); + } + } + break; + + /* Or an end tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "hr", "iframe", "image", "img", + "input", "isindex", "noembed", "noframes", "param", "select", + "spacer", "table", "textarea", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'hr': + case 'iframe': + case 'image': + case 'img': + case 'input': + case 'isindex': + case 'noembed': + case 'noframes': + case 'param': + case 'select': + case 'spacer': + case 'table': + case 'textarea': + case 'wbr': + // Parse error. Ignore the token. + break; + + /* An end tag token not covered by the previous entries */ + default: + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + /* Initialise node to be the current node (the bottommost + node of the stack). */ + $node = end($this->stack); + + /* If node has the same tag name as the end tag token, + then: */ + if ($token['name'] === $node->nodeName) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* If the tag name of the end tag token does not + match the tag name of the current node, this is a + parse error. */ + // k + + /* Pop all the nodes from the current node up to + node, including node, then stop this algorithm. */ + for ($x = count($this->stack) - $n; $x >= $n; $x--) { + array_pop($this->stack); + } + + } else { + $category = $this->getElementCategory($node); + + if ($category !== self::SPECIAL && $category !== self::SCOPING) { + /* Otherwise, if node is in neither the formatting + category nor the phrasing category, then this is a + parse error. Stop this algorithm. The end tag token + is ignored. */ + return false; + } + } + } + break; + } + break; + } + } + + private function inTable($token) + { + $clear = array('html', 'table'); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "caption" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'caption' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + /* Insert an HTML element for the token, then switch the + insertion mode to "in caption". */ + $this->insertElement($token); + $this->mode = self::IN_CAPTION; + + /* A start tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'colgroup' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the + insertion mode to "in column group". */ + $this->insertElement($token); + $this->mode = self::IN_CGROUP; + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'col' + ) { + $this->inTable( + array( + 'name' => 'colgroup', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + $this->inColumnGroup($token); + + /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('tbody', 'tfoot', 'thead') + ) + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in table body". */ + $this->insertElement($token); + $this->mode = self::IN_TBODY; + + /* A start tag whose tag name is one of: "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && + in_array($token['name'], array('td', 'th', 'tr')) + ) { + /* Act as if a start tag token with the tag name "tbody" had been + seen, then reprocess the current token. */ + $this->inTable( + array( + 'name' => 'tbody', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inTableBody($token); + + /* A start tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'table' + ) { + /* Parse error. Act as if an end tag token with the tag name "table" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inTable( + array( + 'name' => 'table', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + + /* An end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + return false; + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a table element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a table element has been + popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'table') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'caption', + 'col', + 'colgroup', + 'html', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Parse error. Process the token as if the insertion mode was "in + body", with the following exception: */ + + /* If the current node is a table, tbody, tfoot, thead, or tr + element, then, whenever a node would be inserted into the current + node, it must instead be inserted into the foster parent element. */ + if (in_array( + end($this->stack)->nodeName, + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* The foster parent element is the parent element of the last + table element in the stack of open elements, if there is a + table element and it has such a parent element. If there is no + table element in the stack of open elements (innerHTML case), + then the foster parent element is the first element in the + stack of open elements (the html element). Otherwise, if there + is a table element in the stack of open elements, but the last + table element in the stack of open elements has no parent, or + its parent node is not an element, then the foster parent + element is the element before the last table element in the + stack of open elements. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table') { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $table->parentNode !== null) { + $this->foster_parent = $table->parentNode; + + } elseif (!isset($table)) { + $this->foster_parent = $this->stack[0]; + + } elseif (isset($table) && ($table->parentNode === null || + $table->parentNode->nodeType !== XML_ELEMENT_NODE) + ) { + $this->foster_parent = $this->stack[$n - 1]; + } + } + + $this->inBody($token); + } + } + + private function inCaption($token) + { + /* An end tag whose tag name is "caption" */ + if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a caption element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a caption element has + been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === 'caption') { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag + name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + )) || ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table') + ) { + /* Parse error. Act as if an end tag with the tag name "caption" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inCaption( + array( + 'name' => 'caption', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + + /* An end tag whose tag name is one of: "body", "col", "colgroup", + "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'col', + 'colgroup', + 'html', + 'tbody', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inColumnGroup($token) + { + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { + /* Insert a col element for the token. Immediately pop the current + node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + /* An end tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'colgroup' + ) { + /* If the current node is the root html element, then this is a + parse error, ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + /* Otherwise, pop the current node (which will be a colgroup + element) from the stack of open elements. Switch the insertion + mode to "in table". */ + } else { + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* An end tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Act as if an end tag with the tag name "colgroup" had been seen, + and then, if that token wasn't ignored, reprocess the current token. */ + $this->inColumnGroup( + array( + 'name' => 'colgroup', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + } + } + + private function inTableBody($token) + { + $clear = array('tbody', 'tfoot', 'thead', 'html'); + + /* A start tag whose tag name is "tr" */ + if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Insert a tr element for the token, then switch the insertion + mode to "in row". */ + $this->insertElement($token); + $this->mode = self::IN_ROW; + + /* A start tag whose tag name is one of: "th", "td" */ + } elseif ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Parse error. Act as if a start tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inTableBody( + array( + 'name' => 'tr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inRow($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node from the stack of open elements. Switch + the insertion mode to "in table". */ + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') + )) || + ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') + ) { + /* If the stack of open elements does not have a tbody, thead, or + tfoot element in table scope, this is a parse error. Ignore the + token. (innerHTML case) */ + if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Act as if an end tag with the same tag name as the current + node ("tbody", "tfoot", or "thead") had been seen, then + reprocess the current token. */ + $this->inTableBody( + array( + 'name' => end($this->stack)->nodeName, + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inRow($token) + { + $clear = array('tr', 'html'); + + /* A start tag whose tag name is one of: "th", "td" */ + if ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in cell". */ + $this->insertElement($token); + $this->mode = self::IN_CELL; + + /* Insert a marker at the end of the list of active formatting + elements. */ + $this->a_formatting[] = self::MARKER; + + /* An end tag whose tag name is "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node (which will be a tr element) from the + stack of open elements. Switch the insertion mode to "in table + body". */ + array_pop($this->stack); + $this->mode = self::IN_TBODY; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* Act as if an end tag with the tag name "tr" had been seen, then, + if that token wasn't ignored, reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Otherwise, act as if an end tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inCell($token) + { + /* An end tag whose tag name is one of: "td", "th" */ + if ($token['type'] === HTML5::ENDTAG && + ($token['name'] === 'td' || $token['name'] === 'th') + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token, then this is a + parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Generate implied end tags, except for elements with the same + tag name as the token. */ + $this->generateImpliedEndTags(array($token['name'])); + + /* Now, if the current node is not an element with the same tag + name as the token, then this is a parse error. */ + // k + + /* Pop elements from this stack until an element with the same + tag name as the token has been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === $token['name']) { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in row". (The current node + will be a tr element at this point.) */ + $this->mode = self::IN_ROW; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html') + ) + ) { + /* Parse error. Ignore the token. */ + + /* An end tag whose tag name is one of: "table", "tbody", "tfoot", + "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token (which can only + happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), + then this is a parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inSelect($token) + { + /* Handle the token as follows: */ + + /* A character token */ + if ($token['type'] === HTML5::CHARACTR) { + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* A start tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'optgroup' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, act as if an end tag + with the tag name "optgroup" had been seen. */ + if (end($this->stack)->nodeName === 'optgroup') { + $this->inSelect( + array( + 'name' => 'optgroup', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* An end tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'optgroup' + ) { + /* First, if the current node is an option element, and the node + immediately before it in the stack of open elements is an optgroup + element, then act as if an end tag with the tag name "option" had + been seen. */ + $elements_in_stack = count($this->stack); + + if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && + $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' + ) { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if ($this->stack[$elements_in_stack - 1] === 'optgroup') { + array_pop($this->stack); + } + + /* An end tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if (end($this->stack)->nodeName === 'option') { + array_pop($this->stack); + } + + /* An end tag whose tag name is "select" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'select' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // w/e + + /* Otherwise: */ + } else { + /* Pop elements from the stack of open elements until a select + element has been popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'select') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* A start tag whose tag name is "select" */ + } elseif ($token['name'] === 'select' && + $token['type'] === HTML5::STARTTAG + ) { + /* Parse error. Act as if the token had been an end tag with the + tag name "select" instead. */ + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + /* An end tag whose tag name is one of: "caption", "table", "tbody", + "tfoot", "thead", "tr", "td", "th" */ + } elseif (in_array( + $token['name'], + array( + 'caption', + 'table', + 'tbody', + 'tfoot', + 'thead', + 'tr', + 'td', + 'th' + ) + ) && $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. */ + // w/e + + /* If the stack of open elements has an element in table scope with + the same tag name as that of the token, then act as if an end tag + with the tag name "select" had been seen, and reprocess the token. + Otherwise, ignore the token. */ + if ($this->elementInScope($token['name'], true)) { + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + $this->mainPhase($token); + } + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterBody($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed if the insertion mode + was "in body". */ + $this->inBody($token); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the first element in the stack of open + elements (the html element), with the data attribute set to the + data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->stack[0]->appendChild($comment); + + /* An end tag with the tag name "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { + /* If the parser was originally created in order to handle the + setting of an element's innerHTML attribute, this is a parse error; + ignore the token. (The element will be an html element in this + case.) (innerHTML case) */ + + /* Otherwise, switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* Anything else */ + } else { + /* Parse error. Set the insertion mode to "in body" and reprocess + the token. */ + $this->mode = self::IN_BODY; + return $this->inBody($token); + } + } + + private function inFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::STARTTAG + ) { + $this->insertElement($token); + + /* An end tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::ENDTAG + ) { + /* If the current node is the root html element, then this is a + parse error; ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + } else { + /* Otherwise, pop the current node from the stack of open + elements. */ + array_pop($this->stack); + + /* If the parser was not originally created in order to handle + the setting of an element's innerHTML attribute (innerHTML case), + and the current node is no longer a frameset element, then change + the insertion mode to "after frameset". */ + $this->mode = self::AFTR_FRAME; + } + + /* A start tag with the tag name "frame" */ + } elseif ($token['name'] === 'frame' && + $token['type'] === HTML5::STARTTAG + ) { + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* An end tag with the tag name "html" */ + } elseif ($token['name'] === 'html' && + $token['type'] === HTML5::ENDTAG + ) { + /* Switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function trailingEndPhase($token) + { + /* After the main phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed in the main phase. */ + $this->mainPhase($token); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or a start tag token. Or an end tag token. */ + } elseif (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. Switch back to the main phase and reprocess the + token. */ + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* OMG DONE!! */ + } + } + + private function insertElement($token, $append = true, $check = false) + { + // Proprietary workaround for libxml2's limitations with tag names + if ($check) { + // Slightly modified HTML5 tag-name modification, + // removing anything that's not an ASCII letter, digit, or hyphen + $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); + // Remove leading hyphens and numbers + $token['name'] = ltrim($token['name'], '-0..9'); + // In theory, this should ever be needed, but just in case + if ($token['name'] === '') { + $token['name'] = 'span'; + } // arbitrary generic choice + } + + $el = $this->dom->createElement($token['name']); + + foreach ($token['attr'] as $attr) { + if (!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], (string)$attr['value']); + } + } + + $this->appendToRealParent($el); + $this->stack[] = $el; + + return $el; + } + + private function insertText($data) + { + $text = $this->dom->createTextNode($data); + $this->appendToRealParent($text); + } + + private function insertComment($data) + { + $comment = $this->dom->createComment($data); + $this->appendToRealParent($comment); + } + + private function appendToRealParent($node) + { + if ($this->foster_parent === null) { + end($this->stack)->appendChild($node); + + } elseif ($this->foster_parent !== null) { + /* If the foster parent element is the parent element of the + last table element in the stack of open elements, then the new + node must be inserted immediately before the last table element + in the stack of open elements in the foster parent element; + otherwise, the new node must be appended to the foster parent + element. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table' && + $this->stack[$n]->parentNode !== null + ) { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { + $this->foster_parent->insertBefore($node, $table); + } else { + $this->foster_parent->appendChild($node); + } + + $this->foster_parent = null; + } + } + + private function elementInScope($el, $table = false) + { + if (is_array($el)) { + foreach ($el as $element) { + if ($this->elementInScope($element, $table)) { + return true; + } + } + + return false; + } + + $leng = count($this->stack); + + for ($n = 0; $n < $leng; $n++) { + /* 1. Initialise node to be the current node (the bottommost node of + the stack). */ + $node = $this->stack[$leng - 1 - $n]; + + if ($node->tagName === $el) { + /* 2. If node is the target node, terminate in a match state. */ + return true; + + } elseif ($node->tagName === 'table') { + /* 3. Otherwise, if node is a table element, terminate in a failure + state. */ + return false; + + } elseif ($table === true && in_array( + $node->tagName, + array( + 'caption', + 'td', + 'th', + 'button', + 'marquee', + 'object' + ) + ) + ) { + /* 4. Otherwise, if the algorithm is the "has an element in scope" + variant (rather than the "has an element in table scope" variant), + and node is one of the following, terminate in a failure state. */ + return false; + + } elseif ($node === $node->ownerDocument->documentElement) { + /* 5. Otherwise, if node is an html element (root element), terminate + in a failure state. (This can only happen if the node is the topmost + node of the stack of open elements, and prevents the next step from + being invoked if there are no more elements in the stack.) */ + return false; + } + + /* Otherwise, set node to the previous entry in the stack of open + elements and return to step 2. (This will never fail, since the loop + will always terminate in the previous step if the top of the stack + is reached.) */ + } + } + + private function reconstructActiveFormattingElements() + { + /* 1. If there are no entries in the list of active formatting elements, + then there is nothing to reconstruct; stop this algorithm. */ + $formatting_elements = count($this->a_formatting); + + if ($formatting_elements === 0) { + return false; + } + + /* 3. Let entry be the last (most recently added) element in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. If the last (most recently added) entry in the list of active + formatting elements is a marker, or if it is an element that is in the + stack of open elements, then there is nothing to reconstruct; stop this + algorithm. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + return false; + } + + for ($a = $formatting_elements - 1; $a >= 0; true) { + /* 4. If there are no entries before entry in the list of active + formatting elements, then jump to step 8. */ + if ($a === 0) { + $step_seven = false; + break; + } + + /* 5. Let entry be the entry one earlier than entry in the list of + active formatting elements. */ + $a--; + $entry = $this->a_formatting[$a]; + + /* 6. If entry is neither a marker nor an element that is also in + thetack of open elements, go to step 4. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + break; + } + } + + while (true) { + /* 7. Let entry be the element one later than entry in the list of + active formatting elements. */ + if (isset($step_seven) && $step_seven === true) { + $a++; + $entry = $this->a_formatting[$a]; + } + + /* 8. Perform a shallow clone of the element entry to obtain clone. */ + $clone = $entry->cloneNode(); + + /* 9. Append clone to the current node and push it onto the stack + of open elements so that it is the new current node. */ + end($this->stack)->appendChild($clone); + $this->stack[] = $clone; + + /* 10. Replace the entry for entry in the list with an entry for + clone. */ + $this->a_formatting[$a] = $clone; + + /* 11. If the entry for clone in the list of active formatting + elements is not the last entry in the list, return to step 7. */ + if (end($this->a_formatting) !== $clone) { + $step_seven = true; + } else { + break; + } + } + } + + private function clearTheActiveFormattingElementsUpToTheLastMarker() + { + /* When the steps below require the UA to clear the list of active + formatting elements up to the last marker, the UA must perform the + following steps: */ + + while (true) { + /* 1. Let entry be the last (most recently added) entry in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. Remove entry from the list of active formatting elements. */ + array_pop($this->a_formatting); + + /* 3. If entry was a marker, then stop the algorithm at this point. + The list has been cleared up to the last marker. */ + if ($entry === self::MARKER) { + break; + } + } + } + + private function generateImpliedEndTags($exclude = array()) + { + /* When the steps below require the UA to generate implied end tags, + then, if the current node is a dd element, a dt element, an li element, + a p element, a td element, a th element, or a tr element, the UA must + act as if an end tag with the respective tag name had been seen and + then generate implied end tags again. */ + $node = end($this->stack); + $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); + + while (in_array(end($this->stack)->nodeName, $elements)) { + array_pop($this->stack); + } + } + + private function getElementCategory($node) + { + $name = $node->tagName; + if (in_array($name, $this->special)) { + return self::SPECIAL; + } elseif (in_array($name, $this->scoping)) { + return self::SCOPING; + } elseif (in_array($name, $this->formatting)) { + return self::FORMATTING; + } else { + return self::PHRASING; + } + } + + private function clearStackToTableContext($elements) + { + /* When the steps above require the UA to clear the stack back to a + table context, it means that the UA must, while the current node is not + a table element or an html element, pop elements from the stack of open + elements. If this causes any elements to be popped from the stack, then + this is a parse error. */ + while (true) { + $node = end($this->stack)->nodeName; + + if (in_array($node, $elements)) { + break; + } else { + array_pop($this->stack); + } + } + } + + private function resetInsertionMode() + { + /* 1. Let last be false. */ + $last = false; + $leng = count($this->stack); + + for ($n = $leng - 1; $n >= 0; $n--) { + /* 2. Let node be the last node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 3. If node is the first node in the stack of open elements, then + set last to true. If the element whose innerHTML attribute is being + set is neither a td element nor a th element, then set node to the + element whose innerHTML attribute is being set. (innerHTML case) */ + if ($this->stack[0]->isSameNode($node)) { + $last = true; + } + + /* 4. If node is a select element, then switch the insertion mode to + "in select" and abort these steps. (innerHTML case) */ + if ($node->nodeName === 'select') { + $this->mode = self::IN_SELECT; + break; + + /* 5. If node is a td or th element, then switch the insertion mode + to "in cell" and abort these steps. */ + } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { + $this->mode = self::IN_CELL; + break; + + /* 6. If node is a tr element, then switch the insertion mode to + "in row" and abort these steps. */ + } elseif ($node->nodeName === 'tr') { + $this->mode = self::IN_ROW; + break; + + /* 7. If node is a tbody, thead, or tfoot element, then switch the + insertion mode to "in table body" and abort these steps. */ + } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { + $this->mode = self::IN_TBODY; + break; + + /* 8. If node is a caption element, then switch the insertion mode + to "in caption" and abort these steps. */ + } elseif ($node->nodeName === 'caption') { + $this->mode = self::IN_CAPTION; + break; + + /* 9. If node is a colgroup element, then switch the insertion mode + to "in column group" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'colgroup') { + $this->mode = self::IN_CGROUP; + break; + + /* 10. If node is a table element, then switch the insertion mode + to "in table" and abort these steps. */ + } elseif ($node->nodeName === 'table') { + $this->mode = self::IN_TABLE; + break; + + /* 11. If node is a head element, then switch the insertion mode + to "in body" ("in body"! not "in head"!) and abort these steps. + (innerHTML case) */ + } elseif ($node->nodeName === 'head') { + $this->mode = self::IN_BODY; + break; + + /* 12. If node is a body element, then switch the insertion mode to + "in body" and abort these steps. */ + } elseif ($node->nodeName === 'body') { + $this->mode = self::IN_BODY; + break; + + /* 13. If node is a frameset element, then switch the insertion + mode to "in frameset" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'frameset') { + $this->mode = self::IN_FRAME; + break; + + /* 14. If node is an html element, then: if the head element + pointer is null, switch the insertion mode to "before head", + otherwise, switch the insertion mode to "after head". In either + case, abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'html') { + $this->mode = ($this->head_pointer === null) + ? self::BEFOR_HEAD + : self::AFTER_HEAD; + + break; + + /* 15. If last is true, then set the insertion mode to "in body" + and abort these steps. (innerHTML case) */ + } elseif ($last) { + $this->mode = self::IN_BODY; + break; + } + } + } + + private function closeCell() + { + /* If the stack of open elements has a td or th element in table scope, + then act as if an end tag token with that tag name had been seen. */ + foreach (array('td', 'th') as $cell) { + if ($this->elementInScope($cell, true)) { + $this->inCell( + array( + 'name' => $cell, + 'type' => HTML5::ENDTAG + ) + ); + + break; + } + } + } + + public function save() + { + return $this->dom; + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php new file mode 100644 index 0000000..3995fec --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php @@ -0,0 +1,49 @@ +data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php new file mode 100644 index 0000000..6cbf56d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php @@ -0,0 +1,59 @@ + form or the form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php new file mode 100644 index 0000000..aec9166 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php @@ -0,0 +1,54 @@ +data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php new file mode 100644 index 0000000..18c8bbb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php @@ -0,0 +1,111 @@ +preserve[$i] = true; + } + for ($i = 65; $i <= 90; $i++) { // upper-case + $this->preserve[$i] = true; + } + for ($i = 97; $i <= 122; $i++) { // lower-case + $this->preserve[$i] = true; + } + $this->preserve[45] = true; // Dash - + $this->preserve[46] = true; // Period . + $this->preserve[95] = true; // Underscore _ + $this->preserve[126]= true; // Tilde ~ + + // extra letters not to escape + if ($preserve !== false) { + for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { + $this->preserve[ord($preserve[$i])] = true; + } + } + } + + /** + * Our replacement for urlencode, it encodes all non-reserved characters, + * as well as any extra characters that were instructed to be preserved. + * @note + * Assumes that the string has already been normalized, making any + * and all percent escape sequences valid. Percents will not be + * re-escaped, regardless of their status in $preserve + * @param string $string String to be encoded + * @return string Encoded string. + */ + public function encode($string) + { + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) { + $ret .= '%' . sprintf('%02X', $int); + } else { + $ret .= $string[$i]; + } + } + return $ret; + } + + /** + * Fix up percent-encoding by decoding unreserved characters and normalizing. + * @warning This function is affected by $preserve, even though the + * usual desired behavior is for this not to preserve those + * characters. Be careful when reusing instances of PercentEncoder! + * @param string $string String to normalize + * @return string + */ + public function normalize($string) + { + if ($string == '') { + return ''; + } + $parts = explode('%', $string); + $ret = array_shift($parts); + foreach ($parts as $part) { + $length = strlen($part); + if ($length < 2) { + $ret .= '%25' . $part; + continue; + } + $encoding = substr($part, 0, 2); + $text = substr($part, 2); + if (!ctype_xdigit($encoding)) { + $ret .= '%25' . $part; + continue; + } + $int = hexdec($encoding); + if (isset($this->preserve[$int])) { + $ret .= chr($int) . $text; + continue; + } + $encoding = strtoupper($encoding); + $ret .= '%' . $encoding . $text; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php new file mode 100644 index 0000000..549e4ce --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php @@ -0,0 +1,218 @@ +getAll(); + $context = new HTMLPurifier_Context(); + $this->generator = new HTMLPurifier_Generator($config, $context); + } + + /** + * Main function that renders object or aspect of that object + * @note Parameters vary depending on printer + */ + // function render() {} + + /** + * Returns a start tag + * @param string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + protected function row($name, $value) + { + if (is_bool($value)) { + $value = $value ? 'On' : 'Off'; + } + return + $this->start('tr') . "\n" . + $this->element('th', $name) . "\n" . + $this->element('td', $value) . "\n" . + $this->end('tr'); + } + + /** + * Escapes a string for HTML output. + * @param string $string String to escape + * @return string + */ + protected function escape($string) + { + $string = HTMLPurifier_Encoder::cleanUTF8($string); + $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); + return $string; + } + + /** + * Takes a list of strings and turns them into a single list + * @param string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + protected function listify($array, $polite = false) + { + if (empty($array)) { + return 'None'; + } + $ret = ''; + $i = count($array); + foreach ($array as $value) { + $i--; + $ret .= $value; + if ($i > 0 && !($polite && $i == 1)) { + $ret .= ', '; + } + if ($polite && $i == 1) { + $ret .= 'and '; + } + } + return $ret; + } + + /** + * Retrieves the class of an object without prefixes, as well as metadata + * @param object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + protected function getClass($obj, $sec_prefix = '') + { + static $five = null; + if ($five === null) { + $five = version_compare(PHP_VERSION, '5', '>='); + } + $prefix = 'HTMLPurifier_' . $sec_prefix; + if (!$five) { + $prefix = strtolower($prefix); + } + $class = str_replace($prefix, '', get_class($obj)); + $lclass = strtolower($class); + $class .= '('; + switch ($lclass) { + case 'enum': + $values = array(); + foreach ($obj->valid_values as $value => $bool) { + $values[] = $value; + } + $class .= implode(', ', $values); + break; + case 'css_composite': + $values = array(); + foreach ($obj->defs as $def) { + $values[] = $this->getClass($def, $sec_prefix); + } + $class .= implode(', ', $values); + break; + case 'css_multiple': + $class .= $this->getClass($obj->single, $sec_prefix) . ', '; + $class .= $obj->max; + break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) { + $class .= ', !important'; + } + break; + } + $class .= ')'; + return $class; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php new file mode 100644 index 0000000..29505fe --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php @@ -0,0 +1,44 @@ +def = $config->getCSSDefinition(); + $ret = ''; + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + $ret .= $this->start('table'); + + $ret .= $this->element('caption', 'Properties ($info)'); + + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Property', array('class' => 'heavy')); + $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + + ksort($this->def->info); + foreach ($this->def->info as $property => $obj) { + $name = $this->getClass($obj, 'AttrDef_'); + $ret .= $this->row($property, $name); + } + + $ret .= $this->end('table'); + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css new file mode 100644 index 0000000..3ff1a88 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css @@ -0,0 +1,10 @@ + +.hp-config {} + +.hp-config tbody th {text-align:right; padding-right:0.5em;} +.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} +.hp-config .namespace th {text-align:center;} +.hp-config .verbose {display:none;} +.hp-config .controls {text-align:center;} + +/* vim: et sw=4 sts=4 */ diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js new file mode 100644 index 0000000..cba00c9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js @@ -0,0 +1,5 @@ +function toggleWriteability(id_of_patient, checked) { + document.getElementById(id_of_patient).disabled = checked; +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php new file mode 100644 index 0000000..4c3ce17 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php @@ -0,0 +1,456 @@ +docURL = $doc_url; + $this->name = $name; + $this->compress = $compress; + // initialize sub-printers + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::C_BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); + } + + /** + * Sets default column and row size for textareas in sub-printers + * @param $cols Integer columns of textarea, null to use default + * @param $rows Integer rows of textarea, null to use default + */ + public function setTextareaDimensions($cols = null, $rows = null) + { + if ($cols) { + $this->fields['default']->cols = $cols; + } + if ($rows) { + $this->fields['default']->rows = $rows; + } + } + + /** + * Retrieves styling, in case it is not accessible by webserver + */ + public static function getCSS() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); + } + + /** + * Retrieves JavaScript, in case it is not accessible by webserver + */ + public static function getJavaScript() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); + } + + /** + * Returns HTML output for a configuration form + * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. + * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. + * @param bool $render_controls + * @return string + */ + public function render($config, $allowed = true, $render_controls = true) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + + $this->config = $config; + $this->genConfig = $gen_config; + $this->prepareGenerator($gen_config); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); + $all = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $all[$ns][$directive] = $config->get($ns . '.' . $directive); + } + + $ret = ''; + $ret .= $this->start('table', array('class' => 'hp-config')); + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); + $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + foreach ($all as $ns => $directives) { + $ret .= $this->renderNamespace($ns, $directives); + } + if ($render_controls) { + $ret .= $this->start('tbody'); + $ret .= $this->start('tr'); + $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); + $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); + $ret .= '[Reset]'; + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a single namespace + * @param $ns String namespace name + * @param array $directives array of directives to values + * @return string + */ + protected function renderNamespace($ns, $directives) + { + $ret = ''; + $ret .= $this->start('tbody', array('class' => 'namespace')); + $ret .= $this->start('tr'); + $ret .= $this->element('th', $ns, array('colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + $ret .= $this->start('tbody'); + foreach ($directives as $directive => $value) { + $ret .= $this->start('tr'); + $ret .= $this->start('th'); + if ($this->docURL) { + $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); + $ret .= $this->start('a', array('href' => $url)); + } + $attr = array('for' => "{$this->name}:$ns.$directive"); + + // crop directive name if it's too long + if (!$this->compress || (strlen($directive) < $this->compress)) { + $directive_disp = $directive; + } else { + $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; + $attr['title'] = $directive; + } + + $ret .= $this->element( + 'label', + $directive_disp, + // component printers must create an element with this id + $attr + ); + if ($this->docURL) { + $ret .= $this->end('a'); + } + $ret .= $this->end('th'); + + $ret .= $this->start('td'); + $def = $this->config->def->info["$ns.$directive"]; + if (is_int($def)) { + $allow_null = $def < 0; + $type = abs($def); + } else { + $type = $def->type; + $allow_null = isset($def->allow_null); + } + if (!isset($this->fields[$type])) { + $type = 0; + } // default + $type_obj = $this->fields[$type]; + if ($allow_null) { + $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); + } + $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + } + $ret .= $this->end('tbody'); + return $ret; + } + +} + +/** + * Printer decorator for directives that accept null + */ +class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer +{ + /** + * Printer being decorated + * @type HTMLPurifier_Printer + */ + protected $obj; + + /** + * @param HTMLPurifier_Printer $obj Printer to decorate + */ + public function __construct($obj) + { + parent::__construct(); + $this->obj = $obj; + } + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + + $ret = ''; + $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Null/Disabled'); + $ret .= $this->end('label'); + $attr = array( + 'type' => 'checkbox', + 'value' => '1', + 'class' => 'null-toggle', + 'name' => "$name" . "[Null_$ns.$directive]", + 'id' => "$name:Null_$ns.$directive", + 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! + ); + if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { + // modify inline javascript slightly + $attr['onclick'] = + "toggleWriteability('$name:Yes_$ns.$directive',checked);" . + "toggleWriteability('$name:No_$ns.$directive',checked)"; + } + if ($value === null) { + $attr['checked'] = 'checked'; + } + $ret .= $this->elementEmpty('input', $attr); + $ret .= $this->text(' or '); + $ret .= $this->elementEmpty('br'); + $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); + return $ret; + } +} + +/** + * Swiss-army knife configuration form field printer + */ +class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer +{ + /** + * @type int + */ + public $cols = 18; + + /** + * @type int + */ + public $rows = 5; + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + // this should probably be split up a little + $ret = ''; + $def = $config->def->info["$ns.$directive"]; + if (is_int($def)) { + $type = abs($def); + } else { + $type = $def->type; + } + if (is_array($value)) { + switch ($type) { + case HTMLPurifier_VarParser::LOOKUP: + $array = $value; + $value = array(); + foreach ($array as $val => $b) { + $value[] = $val; + } + //TODO does this need a break? + case HTMLPurifier_VarParser::ALIST: + $value = implode(PHP_EOL, $value); + break; + case HTMLPurifier_VarParser::HASH: + $nvalue = ''; + foreach ($value as $i => $v) { + if (is_array($v)) { + // HACK + $v = implode(";", $v); + } + $nvalue .= "$i:$v" . PHP_EOL; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($type === HTMLPurifier_VarParser::C_MIXED) { + return 'Not supported'; + $value = serialize($value); + } + $attr = array( + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:$ns.$directive" + ); + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + if (isset($def->allowed)) { + $ret .= $this->start('select', $attr); + foreach ($def->allowed as $val => $b) { + $attr = array(); + if ($value == $val) { + $attr['selected'] = 'selected'; + } + $ret .= $this->element('option', $val, $attr); + } + $ret .= $this->end('select'); + } elseif ($type === HTMLPurifier_VarParser::TEXT || + $type === HTMLPurifier_VarParser::ITEXT || + $type === HTMLPurifier_VarParser::ALIST || + $type === HTMLPurifier_VarParser::HASH || + $type === HTMLPurifier_VarParser::LOOKUP) { + $attr['cols'] = $this->cols; + $attr['rows'] = $this->rows; + $ret .= $this->start('textarea', $attr); + $ret .= $this->text($value); + $ret .= $this->end('textarea'); + } else { + $attr['value'] = $value; + $attr['type'] = 'text'; + $ret .= $this->elementEmpty('input', $attr); + } + return $ret; + } +} + +/** + * Bool form field printer + */ +class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer +{ + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + $ret = ''; + $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); + + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Yes'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:Yes_$ns.$directive", + 'value' => '1' + ); + if ($value === true) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' No'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:No_$ns.$directive", + 'value' => '0' + ); + if ($value === false) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php new file mode 100644 index 0000000..ae86391 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php @@ -0,0 +1,324 @@ +config =& $config; + + $this->def = $config->getHTMLDefinition(); + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + + $ret .= $this->renderDoctype(); + $ret .= $this->renderEnvironment(); + $ret .= $this->renderContentSets(); + $ret .= $this->renderInfo(); + + $ret .= $this->end('div'); + + return $ret; + } + + /** + * Renders the Doctype table + * @return string + */ + protected function renderDoctype() + { + $doctype = $this->def->doctype; + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Doctype'); + $ret .= $this->row('Name', $doctype->name); + $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); + $ret .= $this->row('Default Modules', implode(', ', $doctype->modules)); + $ret .= $this->row('Default Tidy Modules', implode(', ', $doctype->tidyModules)); + $ret .= $this->end('table'); + return $ret; + } + + + /** + * Renders environment table, which is miscellaneous info + * @return string + */ + protected function renderEnvironment() + { + $def = $this->def; + + $ret = ''; + + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Environment'); + + $ret .= $this->row('Parent of fragment', $def->info_parent); + $ret .= $this->renderChildren($def->info_parent_def->child); + $ret .= $this->row('Block wrap name', $def->info_block_wrapper); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Global attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Tag transforms'); + $list = array(); + foreach ($def->info_tag_transform as $old => $new) { + $new = $this->getClass($new, 'TagTransform_'); + $list[] = "<$old> with $new"; + } + $ret .= $this->element('td', $this->listify($list)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); + $ret .= $this->end('tr'); + + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Content Sets table + * @return string + */ + protected function renderContentSets() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Content Sets'); + foreach ($this->def->info_content_sets as $name => $lookup) { + $ret .= $this->heavyHeader($name); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($lookup)); + $ret .= $this->end('tr'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Elements ($info) table + * @return string + */ + protected function renderInfo() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Elements ($info)'); + ksort($this->def->info); + $ret .= $this->heavyHeader('Allowed tags', 2); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); + $ret .= $this->end('tr'); + foreach ($this->def->info as $name => $def) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Inline content'); + $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); + $ret .= $this->end('tr'); + if (!empty($def->excludes)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Excludes'); + $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_pre)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_post)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); + $ret .= $this->end('tr'); + } + if (!empty($def->auto_close)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Auto closed by'); + $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); + $ret .= $this->end('tr'); + } + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Allowed attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); + $ret .= $this->end('tr'); + + if (!empty($def->required_attr)) { + $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); + } + + $ret .= $this->renderChildren($def->child); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a row describing the allowed children of an element + * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element + * @return string + */ + protected function renderChildren($def) + { + $context = new HTMLPurifier_Context(); + $ret = ''; + $ret .= $this->start('tr'); + $elements = array(); + $attr = array(); + if (isset($def->elements)) { + if ($def->type == 'strictblockquote') { + $def->validateChildren(array(), $this->config, $context); + } + $elements = $def->elements; + } + if ($def->type == 'chameleon') { + $attr['rowspan'] = 2; + } elseif ($def->type == 'empty') { + $elements = array(); + } elseif ($def->type == 'table') { + $elements = array_flip( + array( + 'col', + 'caption', + 'colgroup', + 'thead', + 'tfoot', + 'tbody', + 'tr' + ) + ); + } + $ret .= $this->element('th', 'Allowed children', $attr); + + if ($def->type == 'chameleon') { + + $ret .= $this->element( + 'td', + 'Block: ' . + $this->escape($this->listifyTagLookup($def->block->elements)), + null, + 0 + ); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element( + 'td', + 'Inline: ' . + $this->escape($this->listifyTagLookup($def->inline->elements)), + null, + 0 + ); + + } elseif ($def->type == 'custom') { + + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $def->dtd_regex + ); + + } else { + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $this->escape($this->listifyTagLookup($elements)), + null, + 0 + ); + } + $ret .= $this->end('tr'); + return $ret; + } + + /** + * Listifies a tag lookup table. + * @param array $array Tag lookup array in form of array('tagname' => true) + * @return string + */ + protected function listifyTagLookup($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $discard) { + if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { + continue; + } + $list[] = $name; + } + return $this->listify($list); + } + + /** + * Listifies a list of objects by retrieving class names and internal state + * @param array $array List of objects + * @return string + * @todo Also add information about internal state + */ + protected function listifyObjectList($array) + { + ksort($array); + $list = array(); + foreach ($array as $obj) { + $list[] = $this->getClass($obj, 'AttrTransform_'); + } + return $this->listify($list); + } + + /** + * Listifies a hash of attributes to AttrDef classes + * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) + * @return string + */ + protected function listifyAttr($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $obj) { + if ($obj === false) { + continue; + } + $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; + } + return $this->listify($list); + } + + /** + * Creates a heavy header row + * @param string $text + * @param int $num + * @return string + */ + protected function heavyHeader($text, $num = 1) + { + $ret = ''; + $ret .= $this->start('tr'); + $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); + $ret .= $this->end('tr'); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php new file mode 100644 index 0000000..189348f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php @@ -0,0 +1,122 @@ +parent = $parent; + } + + /** + * Recursively retrieves the value for a key + * @param string $name + * @throws HTMLPurifier_Exception + */ + public function get($name) + { + if ($this->has($name)) { + return $this->data[$name]; + } + // possible performance bottleneck, convert to iterative if necessary + if ($this->parent) { + return $this->parent->get($name); + } + throw new HTMLPurifier_Exception("Key '$name' not found"); + } + + /** + * Sets the value of a key, for this plist + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * Returns true if a given key exists + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->data); + } + + /** + * Resets a value to the value of it's parent, usually the default. If + * no value is specified, the entire plist is reset. + * @param string $name + */ + public function reset($name = null) + { + if ($name == null) { + $this->data = array(); + } else { + unset($this->data[$name]); + } + } + + /** + * Squashes this property list and all of its property lists into a single + * array, and returns the array. This value is cached by default. + * @param bool $force If true, ignores the cache and regenerates the array. + * @return array + */ + public function squash($force = false) + { + if ($this->cache !== null && !$force) { + return $this->cache; + } + if ($this->parent) { + return $this->cache = array_merge($this->parent->squash($force), $this->data); + } else { + return $this->cache = $this->data; + } + } + + /** + * Returns the parent plist. + * @return HTMLPurifier_PropertyList + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the parent plist. + * @param HTMLPurifier_PropertyList $plist Parent plist + */ + public function setParent($plist) + { + $this->parent = $plist; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php new file mode 100644 index 0000000..f68fc8c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php @@ -0,0 +1,43 @@ +l = strlen($filter); + $this->filter = $filter; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function accept() + { + $key = $this->getInnerIterator()->key(); + if (strncmp($key, $this->filter, $this->l) !== 0) { + return false; + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php new file mode 100644 index 0000000..f58db90 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php @@ -0,0 +1,56 @@ +input = $input; + $this->output = array(); + } + + /** + * Shifts an element off the front of the queue. + */ + public function shift() { + if (empty($this->output)) { + $this->output = array_reverse($this->input); + $this->input = array(); + } + if (empty($this->output)) { + return NULL; + } + return array_pop($this->output); + } + + /** + * Pushes an element onto the front of the queue. + */ + public function push($x) { + array_push($this->input, $x); + } + + /** + * Checks if it's empty. + */ + public function isEmpty() { + return empty($this->input) && empty($this->output); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php new file mode 100644 index 0000000..e1ff3b7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php @@ -0,0 +1,26 @@ +strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php new file mode 100644 index 0000000..4414c17 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php @@ -0,0 +1,17 @@ +strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); + $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); + $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); + $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php new file mode 100644 index 0000000..f193933 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php @@ -0,0 +1,181 @@ +getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // setup the context variable 'IsInline', for chameleon processing + // is 'false' when we are not inline, 'true' when it must always + // be inline, and an integer when it is inline for a certain + // branch of the document tree + $is_inline = $definition->info_parent_def->descendants_are_inline; + $context->register('IsInline', $is_inline); + + // setup error collector + $e =& $context->get('ErrorCollector', true); + + //####################################################################// + // Loop initialization + + // stack that contains all elements that are excluded + // it is organized by parent elements, similar to $stack, + // but it is only populated when an element with exclusions is + // processed, i.e. there won't be empty exclusions. + $exclude_stack = array($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php new file mode 100644 index 0000000..f65e352 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php @@ -0,0 +1,659 @@ +getHTMLDefinition(); + + // local variables + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + // used for autoclose early abortion + $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); + $e = $context->get('ErrorCollector', true); + $i = false; // injector index + list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); + if ($token === NULL) { + return array(); + } + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + + // member variables + $this->stack =& $stack; + $this->tokens =& $tokens; + $this->token =& $token; + $this->zipper =& $zipper; + $this->config = $config; + $this->context = $context; + + // context variables + $context->register('CurrentNesting', $stack); + $context->register('InputZipper', $zipper); + $context->register('CurrentToken', $token); + + // -- begin INJECTOR -- + + $this->injectors = array(); + + $injectors = $config->getBatch('AutoFormat'); + $def_injectors = $definition->info_injector; + $custom_injectors = $injectors['Custom']; + unset($injectors['Custom']); // special case + foreach ($injectors as $injector => $b) { + // XXX: Fix with a legitimate lookup table of enabled filters + if (strpos($injector, '.') !== false) { + continue; + } + $injector = "HTMLPurifier_Injector_$injector"; + if (!$b) { + continue; + } + $this->injectors[] = new $injector; + } + foreach ($def_injectors as $injector) { + // assumed to be objects + $this->injectors[] = $injector; + } + foreach ($custom_injectors as $injector) { + if (!$injector) { + continue; + } + if (is_string($injector)) { + $injector = "HTMLPurifier_Injector_$injector"; + $injector = new $injector; + } + $this->injectors[] = $injector; + } + + // give the injectors references to the definition and context + // variables for performance reasons + foreach ($this->injectors as $ix => $injector) { + $error = $injector->prepare($config, $context); + if (!$error) { + continue; + } + array_splice($this->injectors, $ix, 1); // rm the injector + trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); + } + + // -- end INJECTOR -- + + // a note on reprocessing: + // In order to reduce code duplication, whenever some code needs + // to make HTML changes in order to make things "correct", the + // new HTML gets sent through the purifier, regardless of its + // status. This means that if we add a start token, because it + // was totally necessary, we don't have to update nesting; we just + // punt ($reprocess = true; continue;) and it does that for us. + + // isset is in loop because $tokens size changes during loop exec + for (;; + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $token = $zipper->next($token)) { + + // check for a rewind + if (is_int($i)) { + // possibility: disable rewinding if the current token has a + // rewind set on it already. This would offer protection from + // infinite loop, but might hinder some advanced rewinding. + $rewind_offset = $this->injectors[$i]->getRewindOffset(); + if (is_int($rewind_offset)) { + for ($j = 0; $j < $rewind_offset; $j++) { + if (empty($zipper->front)) break; + $token = $zipper->prev($token); + // indicate that other injectors should not process this token, + // but we need to reprocess it. See Note [Injector skips] + unset($token->skip[$i]); + $token->rewind = $i; + if ($token instanceof HTMLPurifier_Token_Start) { + array_pop($this->stack); + } elseif ($token instanceof HTMLPurifier_Token_End) { + $this->stack[] = $token->start; + } + } + } + $i = false; + } + + // handle case of document end + if ($token === NULL) { + // kill processing if stack is empty + if (empty($this->stack)) { + break; + } + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error [TagClosedSuppress] + if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); + } + + // append, don't splice, since this is the end + $token = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; + continue; + } + + //echo '
'; printZipper($zipper, $token);//printTokens($this->stack); + //flush(); + + // quick-check: if it's not a tag, no need to process + if (empty($token->is_tag)) { + if ($token instanceof HTMLPurifier_Token_Text) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + // See Note [Injector skips] + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + // XXX fuckup + $r = $token; + $injector->handleText($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + } + // another possibility is a comment + continue; + } + + if (isset($definition->info[$token->name])) { + $type = $definition->info[$token->name]->child->type; + } else { + $type = false; // Type is unknown, treat accordingly + } + + // quick tag checks: anything that's *not* an end tag + $ok = false; + if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { + // claims to be a start tag but is empty + $token = new HTMLPurifier_Token_Empty( + $token->name, + $token->attr, + $token->line, + $token->col, + $token->armor + ); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + // NB: this assignment is required + $old_token = $token; + $token = new HTMLPurifier_Token_End($token->name); + $token = $this->insertBefore( + new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) + ); + // punt (since we had to modify the input stream in a non-trivial way) + $reprocess = true; + continue; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // real empty token + $ok = true; + } elseif ($token instanceof HTMLPurifier_Token_Start) { + // start tag + + // ...unless they also have to close their parent + if (!empty($this->stack)) { + + // Performance note: you might think that it's rather + // inefficient, recalculating the autoclose information + // for every tag that a token closes (since when we + // do an autoclose, we push a new token into the + // stream and then /process/ that, before + // re-processing this token.) But this is + // necessary, because an injector can make an + // arbitrary transformations to the autoclosing + // tokens we introduce, so things may have changed + // in the meantime. Also, doing the inefficient thing is + // "easy" to reason about (for certain perverse definitions + // of "easy") + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + $parent_def = null; + $parent_elements = null; + $autoclose = false; + if (isset($definition->info[$parent->name])) { + $parent_def = $definition->info[$parent->name]; + $parent_elements = $parent_def->child->getAllowedElements($config); + $autoclose = !isset($parent_elements[$token->name]); + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // Check if an element can be wrapped by another + // element to make it valid in a context (for + // example,