I've my site set up so I just need to add "?lang=en" or "?lang=es" to change languages English / Spanish.
When I enter the site with, for ex, "http://domain.com/something/something_else?lang=es", a cookie is set so I continue to navigate the site in that language.
I would like to redirect my users first by the "Accept-Language" value of their browser, but then allow them to continue to navigate the site in other language if they want to.
What would be the best way to do it? Would .htaccess work along with the cookie that's set when the language is chosen?
EDIT: Here's my updated code with Paul answer:
EDIT2: Oh, I just have "en" and "es" languages. I'm not sure on how this code wpuld choose only between this two or set the default... :/
if (isset($_GET["lang"]))
$this->setLanguage($_GET["lang"]);
elseif (isset($_COOKIE["language"]))
$this->setLanguage($_COOKIE["language"]);
elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
{
// Parse the Accept-Language according to:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
preg_match_all(
'/([a-z]{1,8})' . // First part of language e.g en
'(-[a-z]{1,8})*\s*' . // other parts of language e.g -us
// Optional quality factor
'(;\s*q\s*=\s*((1(\.0{0,3}))|(0(\.[0-9]{0,3}))))?/i',
$_SERVER['HTTP_ACCEPT_LANGUAGE'],
$langParse);
$langs = $langParse[1];
$quals = $langParse[4];
$numLanguages = count($langs);
$langArr = array();
for ($num = 0; $num < $numLanguages; $num++)
{
$newLang = strtoupper($langs[$num]);
$newQual = isset($quals[$num]) ?
(empty($quals[$num]) ? 1.0 : floatval($quals[$num])) : 0.0;
// Choose whether to upgrade or set the quality factor for the
// primary language.
$langArr[$newLang] = (isset($langArr[$newLang])) ?
max($langArr[$newLang], $newQual) : $newQual;
}
// sort list based on value
arsort($langArr, SORT_NUMERIC);
$acceptedLanguages = array_keys($langArr);
$preferredLanguage = reset($acceptedLanguages);
$this->setLanguage($preferredLanguage);
}
else
$this->setLanguage("en");
I do this in PHP. Accept-Language is a complex thing. A browser can suggest more than one language that it would like to accept (each with a quality factor that shows which it prefers). For my site I have a default language to display (which is shown when none of the Accept-Languages is in my translation list). Otherwise if there is no language set (setLang) I choose it based on the most acceptable for the browser by parsing the Accept-Language. The function I use is below (it contains my session manager for setting cookies - but you could reimplement that with direct calls to $_SESSION[etc] = $foo;).
Edit: Unfortunately my website only has translations for the primary languages (EN, ES, FR) rather than (en_US, en_GB, es_MX, es_ES) so I choose the highest quality factor specified in these for the primary language.
public function setLanguage($setLang='')
{
if (!empty($setLang))
{
$this->setup['Session']->set($this->setup['Lang_Key'], $setLang);
}
else
{
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
{
// Parse the Accept-Language according to:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
preg_match_all(
'/([a-z]{1,8})' . // First part of language e.g en
'(-[a-z]{1,8})*\s*' . // other parts of language e.g -us
// Optional quality factor
'(;\s*q\s*=\s*((1(\.0{0,3}))|(0(\.[0-9]{0,3}))))?/i',
$_SERVER['HTTP_ACCEPT_LANGUAGE'],
$langParse);
$langs = $langParse[1];
$quals = $langParse[4];
$numLanguages = count($langs);
$langArr = array();
for ($num = 0; $num < $numLanguages; $num++)
{
$newLang = strtoupper($langs[$num]);
$newQual = isset($quals[$num]) ?
(empty($quals[$num]) ? 1.0 : floatval($quals[$num])) : 0.0;
// Choose whether to upgrade or set the quality factor for the
// primary language.
$langArr[$newLang] = (isset($langArr[$newLang])) ?
max($langArr[$newLang], $newQual) : $newQual;
}
// sort list based on value
arsort($langArr, SORT_NUMERIC);
$acceptedLanguages = array_keys($langArr);
$preferredLanguage = reset($acceptedLanguages);
$this->setup['Session']->set(
$this->setup['Lang_Key'], $preferredLanguage);
}
else
{
$this->setup['Session']->set(
$this->setup['Lang_Key'], $this->setup['Default_Language']);
}
}
return $this->setup['Session']->get($this->setup['Lang_Key']);
}
I do this in PHP. Accept-Language is a complex thing. A browser can suggest more than one language that it would like to accept (each with a quality factor that shows which it prefers).
Unfortunately my website only has translations for the primary languages (EN, ES, FR) rather than (en_US, en_GB, es_MX, es_ES) so I choose the highest quality factor specified in these for the primary language.
Below is an untested edit which should remove most or all of the dependencies from my code. Sorry, things were confusing with my previous answer. I had a few calls to my function with some checking of languages are done elsewhere. The code below should set the session language variable, which you should use elsewhere for determining the correct translation.
It seems a lot less complicated than my previous answer and I will have to implement this in my own code before long. For people who need specific translations (EN_US, EN_GB) then the below code should be modified to take account of Match 2 in the preg_match_all.
$websiteLanguages = array('EN', 'ES');
session_start();
// The user wants a specific language regardless of their accept settings.
if (isset($_GET["lang"]))
{
$_SESSION["language"] = $_GET["lang"];
return;
}
// A language has already been decided upon.
if (isset($_SESSION["language"]))
{
return;
}
// No language has been chosen we should choose from the accept language.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
{
// Parse the Accept-Language according to:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
preg_match_all(
'/([a-z]{1,8})' . // M1 - First part of language e.g en
'(-[a-z]{1,8})*\s*' . // M2 -other parts of language e.g -us
// Optional quality factor M3 ;q=, M4 - Quality Factor
'(;\s*q\s*=\s*((1(\.0{0,3}))|(0(\.[0-9]{0,3}))))?/i',
$_SERVER['HTTP_ACCEPT_LANGUAGE'],
$langParse);
$langs = $langParse[1]; // M1 - First part of language
$quals = $langParse[4]; // M4 - Quality Factor
$numLanguages = count($langs);
$langArr = array();
for ($num = 0; $num < $numLanguages; $num++)
{
$newLang = strtoupper($langs[$num]);
$newQual = isset($quals[$num]) ?
(empty($quals[$num]) ? 1.0 : floatval($quals[$num])) : 0.0;
// Choose whether to upgrade or set the quality factor for the
// primary language.
$langArr[$newLang] = (isset($langArr[$newLang])) ?
max($langArr[$newLang], $newQual) : $newQual;
}
// sort list based on value
// langArr will now be an array like: array('EN' => 1, 'ES' => 0.5)
arsort($langArr, SORT_NUMERIC);
// The languages the client accepts in order of preference.
$acceptedLanguages = array_keys($langArr);
// Set the most preferred language that we have a translation for.
foreach ($acceptedLanguages as $preferredLanguage)
{
if (in_array($preferredLanguage, $websiteLanguages))
{
$_SESSION['Language'] = $preferredLanguage;
return;
}
}
}
// We will have returned by now if a language could be chosen, otherwise use
// our default language.
$_SESSION['Language'] = "EN";
Related
I'm making a new project in Zend 3 that requires me to have a unique ID or HASH which I can use in several places later.
I looked at many examples on Google, and could not find a function that can satisfy my requirements because this needs to be 99% unique all the time, and it needs to be able to generate hundreds, millions of "hashes" unique all the time.
The following function caught my attention:
function uniqidReal($lenght = 13) {
// uniqid gives 13 chars, but you could adjust it to your needs.
if (function_exists("random_bytes")) {
$bytes = random_bytes(ceil($lenght / 2));
} elseif (function_exists("openssl_random_pseudo_bytes")) {
$bytes = openssl_random_pseudo_bytes(ceil($lenght / 2));
} else {
throw new Exception("no cryptographically secure random function available");
}
return substr(bin2hex($bytes), 0, $lenght);
}
A simple test:
echo "<pre>";
for($i = 0; $i < 100; $i++)
{
echo $this->uniqidReal(25) .PHP_EOL ;
}
The result:
a8ba1942ad99d09f496d3d564
5b24746d09cada4b2dc9816bd
c6630c35bc9b4ed0907c803e0
48e04958b633e8a5ead137bb1
643a4ce1bcbca66cea397e85e
d2cd4c6f8dc7054dd0636075f
d9c78bae38720b7e0cc6361f2
54e5f852862adad2ad7bc3349
16c4e42e4f63f62bf9653f96e
c63d64af261e601e4b124e38f
29a3efa07a4d77406349e3020
107d78fdfca13571c152441f2
591b25ebdb695c8259ccc7fe9
105c4f2cc5266bb82222480ba
84e9ad8fd76226f86c89c1ac1
39381d31f494d320abc538a8e
7f8141db50a41b15a85599548
7b15055f6d9fb1228b7438d2a
659182c7bcd5b050befd3fc4c
06f70d134a3839677caa0d246
600b15c9dc53ef7a4551b8a90
a9c8af631c5361e8e1e1b8d9d
4b4b0aca3bbf15d35dd7d1050
f77024a07ee0dcee358dc1f5e
408c007b9d771718263b536e1
2de08e01684805a189224db75
c3838c034ae22d21f27e5d040
b15e9b0bab6ef6a56225a5983
251809396beb9d24b384f5fe8
cec6d262803311152db31b723
95d271ffdfe9df5861eefbaa4
7c11f3401530790b9ef510e55
e363390e2829097e7762bddc4
7ef34c69d9b8e38d72c6db29f
309a84490a7e387aaff1817ca
c214af2927c683954894365df
9f70859880b7ffa4b28265dbb
608e2f2f9e38025d92a1a4f03
c457a54d2da30a4a517edf14c
8670acbded737b1d2febdd954
99899b74b6469e366122b658c
3066408f5b4e86ef84bdb3fb9
010715f4955f66da3402bfa7b
fa01675690435b914631b46e1
2c5e234c5868799f31a6c983c
8345da31809ab2d9714a01d05
7b4e0e507dd0a8b6d7170a265
5aa71aded9fe7afa9a93a98c5
3714fb9f061398d4bb6af909d
165dd0af233cce64cefec12ed
849dda54070b868b50f356068
fe5f6e408eda6e9d429fa34ed
cd13f8da95c5b92b16d9d2781
65d0f69b41ea996ae2f8783a5
5742caf7a922eb3aaa270df30
f381ac4b84f3315e9163f169e
8c2afa1ab32b6fe402bf97ba3
a9f431efe6fc98aa64dbecbc2
8f0746e4e9529326d087f828b
bfc3cbea4d7f5c4495a14fc49
e4bf2d1468c6482570612360e
f1c7238766acdb7f199049487
60ae8a1ffd6784f7bbbc7b437
30afd67f207de6e893f7c9f42
dfa151daccb0e8d64d100f719
07be6a7d4aab21ccd9942401b
73ca1a54fcc40f7a46f46afbd
94ed2888fb93cb65d819d9d52
b7317773c6a15aa0bdf25fa01
edbb7f20f7523d9d941f3ebce
99a3c204b9f2036d3c38342bb
a0585424b8ab2ffcabee299d5
64e669fe2490522451cf10f85
18b8be34d4c560cda5280a103
9524d1f024b3c9864a3fccf75
0e7e94e7974894c98442241bc
4a17cc5e3d2baabaa338f592e
b070eaf38f390516f5cf61aa7
cc7832ea327b7426d8d2b8c2b
0df0a1d4833ebbb5d463c56bf
1bb610a8bb4e241996c9c756a
34ac2fdeb4b88fe6321a1d9c3
f0b20f8e79090dcb65195524c
307252efdd2b833228e0c301f
3908e63b405501782e629ac0b
29e66717adf14fb30c626103d
c8abd48af5f9332b322dffad0
80cd4e162bc7e8fb3a756b48c
825c00cec2294061eb328dd97
106205a2e24609652d149bc17
f1f896657fbc6f6287e7dee20
0fbd16ade658e24d69f76a225
4ab3b5eeeda86fa81afba796a
11d34f3d2ffb61d55da560ddb
013d6151bad187906fcc579a4
4509279a28f34bcf5327dd4c0
3c0eb47b3f9dc5a2f794bb9ad
1e6506906f23542c889330836
e7b1c5012390f3c7c48def9f3
d86caa695cb5fa1e0a2ead4cc
But I cannot confirm that this does guarantee me a 99% success rate for my production environment.
If someone can advise me, or provide me an example I would much appreciate it!
Function random_bytes generates cryptographically secure random bytes
For openssl_random_pseudo_bytes add the crypto_strong paramdeter to ensure the algorithm used is cryptographically strong.
Since your requirement is only 99% unique cryptographically secure random bytes will meet your requirement.
This should be a comment, but its a bit long.
There is some confusion over your use of "unique" and "all the time". A token is either unique or it is not. Using a random number generator to create tokens alone is not sufficient to guarantee uniqueness - the whole point of a random number generator is that you don't know what the next value to be generated will be - meaning you also don't know that the next number won't be the same as a previous number. OTOH, using random_bytes() or openssl_random_pseudo_bytes() to generate a token which is "99% unique all the time" seems like a massive overkill.
To work out how unique this is likely to be we would need to know how many tokens will be considered within the populations at any one time (or to be able to calculate this from the expected rate of creation and the TTL).
That you are using large numbers rather implies you have a very good reason for not using the simplest and most obvious unique identifier - i.e. an incrementing integer. Hence the resistance to guessing an existing identifier is clearly critical to the implementation - but again you've told us nothing about that.
Pasting the title of your post into Google turns up your post as the top result - with PHP's uniqid() function immediately after it - yet for some reason you've either not found uniqid() or have rejected it for some reason.
The title of your post is also an oxymoron - In order to define an infinite set of identifiers, the identifiers would need to be of infinite length.
it needs to be able to generate hundreds, millions of "hashes"
....and you want it all to run within the Zend Framework? - LOL.
But I cannot confirm that this does guarantee me a 99% success rate for my production environment.
Why not? You have sufficient information here to confirm that the bitwise entropy is evenly distributed and should know the planned capacity of the production environment. The rest is basic arithmetic.
We are about 8x10⁹ people. Imagine all us access your site once each second needing a unique identifier during a year. You need about 2,52288×10²³ identifiers. If you think your site will be in production about 1000 years, and population get bigger by a 1000 factor you need about 10²⁹ identifiers; so a 32 bytes auto-incremental string is good enough. Add as suffix a pseudo-random 32 bytes string to get a secure 64 bytes identifier. Doing a bit plus you can hash identifiers to create tokens.
Then is easy to write a function to get them.
Edited 2017/04/13
A small sample:
The first thing you need is a pseudo-random strong keys generator. I'll post the function I'm using currently:
<?php
function pseudoRandomBytes($count = 32){
static $random_state, $bytes, $has_openssl, $has_hash;
$missing_bytes = $count - strlen($bytes);
if ($missing_bytes > 0) {
// If you are using a Php version before 5.3.4 avoid using
// openssl_random_pseudo_bytes()
if (!isset($has_openssl)) {
$has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=')
&& function_exists('openssl_random_pseudo_bytes');
}
// to get entropy
if ($has_openssl) {
$bytes .= openssl_random_pseudo_bytes($missing_bytes);
} elseif ($fh = #fopen('/dev/urandom', 'rb')) {
// avoiding openssl_random_pseudo_bytes()
// you find entropy at /dev/urandom usually available in most
// *nix systems
$bytes .= fread($fh, max(4096, $missing_bytes));
fclose($fh);
}
// If it fails you must create enough entropy
if (strlen($bytes) < $count) {
// Initialize on the first call. The contents of $_SERVER
// includes a mix of user-specific and system information
// that varies a little with each page.
if (!isset($random_state)) {
$random_state = print_r($_SERVER, TRUE);
if (function_exists('getmypid')) {
// Further initialize with the somewhat random PHP process ID.
$random_state .= getmypid();
}
// hash() is only available in PHP 5.1.2+ or via PECL.
$has_hash = function_exists('hash')
&& in_array('sha256', hash_algos());
$bytes = '';
}
if ($has_hash) {
do {
$random_state = hash('sha256', microtime() . mt_rand() .
$random_state);
$bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
} while (strlen($bytes) < $count);
} else {
do {
$random_state = md5(microtime() . mt_rand() . $random_state);
$bytes .= pack("H*", md5(mt_rand() . $random_state));
} while (strlen($bytes) < $count);
}
}
}
$output = substr($bytes, 0, $count);
$bytes = substr($bytes, $count);
return $output;
}
Once you have that function you need a function to create your random keys:
<?php
function pseudo_random_key($byte_count = 32) {
return base64_encode(pseudoRandomBytes($byte_count));
}
As random does not mean unique! you need to merge a unique 32 bytes prefix as I suggested. As big number functions are time-expensive I'll use a chunk-math function using a prefix I suppose generated from time to time using a cron function and stored at an environment DB variable and an auto-incremental index also db-stored
<?php
function uniqueChunkMathKeysPrefix(){
// a call to read your db for prefix
// I suppose you have an environment string-keyed table
// and a couple of dbfunction to read and write data to it
$last18bytesPrefix = dbReadEnvVariable('unique_prefix');
// Also you store your current index wich returns to 0 once you get
// a 99999999999999 value
$lastuniqueindex = dbReadEnvVariable('last_unique_keys_index');
if ($lastuniqueindex < 99999999999999){
$currentuniqueindex = $lastuniqueindex + 1;
$curret18bytesPrefix = $last18bytesPrefix;
}else{
$currentuniqueindex = 0;
$curret18bytesPrefix = dbReadEnvVariable('next_unique_prefix');
// flag your db variables to notify cron to create a new next prefix
dbStoreEnvVariable('next_unique_prefix', 0);
dbStoreEnvVariable('unique_prefix', $curret18bytesPrefix);
// you have the time needed to have site visits and create new
// 99999999999999 keys as a while to run your cron to adjust your
// next prefix
}
// store your current index
dbStoreEnvVariable('last_unique_keys_index', $currentuniqueindex);
// Finally you create the unique index prefix part
$uniqueindexchunk = substr('00000000000000'.$currentuniqueindex, -14);
// return the output
return $curret18bytesPrefix.$uniqueindexchunk;
}
Now you can write a function for unique pseudo-random 64 bytes uniquekeys
<?php
function createUniquePseudoRandomKey(){
$newkey = uniqueChunkMathKeysPrefix() . pseudo_random_key(32);
// to beautify the output make a dummie call
// masking the 0s ties
return md5($newkey);
}
I wanted to know, is there any way from PHP/javascript to get current client OS language. I tried to use $_SERVER["HTTP_ACCEPT_LANGUAGE"] but sometimes it get the wrong language.
For example in Google Chrome:
My OS: Windows 7
Language: English
Using $_SERVER["HTTP_ACCEPT_LANGUAGE"] I got this result:
HTTP_ACCEPT_LANGUAGE: zh,en-US;q=0.8,en;q=0.6
It said "zh" is my primary language.
Is there any other way to get client OS language? Because that's what I wanted, not the browser language setting. Thanks
try this function
function getUserLanguage() {
$langs = array();
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
// break up string into pieces (languages and q factors)
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
$_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse);
if (count($lang_parse[1])) {
// create a list like â??enâ?? => 0.8
$langs = array_combine($lang_parse[1], $lang_parse[4]);
// set default to 1 for any without q factor
foreach ($langs as $lang => $val) {
if ($val === '') $langs[$lang] = 1;
}
// sort list based on value
arsort($langs, SORT_NUMERIC);
}
}
//extract most important (first)
foreach ($langs as $lang => $val) { break; }
//if complex language simplify it
if (stristr($lang,"-")) {$tmp = explode("-",$lang); $lang = $tmp[0]; }
return $lang;
}
Send it via javascript on IE?
navigator.browserLanguage: browser language
navigator.systemLanguage: Windows system language
navigator.userLanguage: Windows user-specific language
Thanks to: Is there anyway to detect OS language using javascript?
That's the only way besides the one you've mentioned to get the language of the client OS, PHP is run by the server and nothing else.
Build a PHP sorting function.
HTTP_ACCEPT_LANGUAGE: zh,en-US;q=0.8,en;q=0.6
zh and en-US share the same q= value, meaning that you can sort on the highest language value and default to en-US if the quality is the same on two languages.
Just noticed that #Quentin mentioned this in the comment section a minute before my edit, well done sir!
Mockup:
$languages = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
$default = 'en-US';
/*
* magic split and structure the language into a array sorted by quality
*
* $languages_sorted_by_quality = array(0.8 => ('zh', 'en-US'));
*/
$top_languages = max($languages_sorted_by_quality);
if (isset($top_languages[$default])) {
$language = $default;
else
$language = $top_languages[0];
have your tried http_negotiate_language
I have an object which has a state property, for example state = 'state4' or state = 'state2'.
Now I also have an array of all available states that the state property can get, state1 to state8 (note: the states are not named stateN. They have eight different names, like payment or canceled. I just put stateN to describe the problem).
In addition to that, I have a logical expression like $expression = !state1||state4&&(!state2||state5) for example. This is the code for the above description:
$state = 'state4';
$expression = '!state1||state4&&(!state2||state5)';
Now I want to check if the logical expression is true or false. In the above case, it's true. In the following case it would be false:
$state = 'state1';
$expression = state4&&!state2||(!state1||state7);
How could this be solved in an elegant way?
//Initialize
$state = 'state4';
$expression = '!state1||state4&&(!state2||state5)';
//Adapt to your needs
$pattern='/state\d/';
//Replace
$e=str_replace($state,'true',$expression);
while (preg_match_all($pattern,$e,$matches)
$e=str_replace($matches[0],'false',$e);
//Eval
eval("\$result=$e;");
echo $result;
Edit:
Your update to the OQ necessitates some minor work:
//Initialize
$state = 'payed';
$expression = '!payed||cancelled&&(!whatever||shipped)';
//Adapt to your needs
$possiblestates=array(
'payed',
'cancelled',
'shipped',
'whatever'
);
//Replace
$e=str_replace($state,'true',$expression);
$e=str_replace($possiblestates,'false',$e);
//Eval
eval("\$result=$e;");
echo $result;
Edit 2
There has been concern about eval and PHP injection in the comments: The expression and the replacements are completly controlled by the application, no user input involved. As long as this holds, eval is safe.
I am using ExpressionLanguage, but there are few different solutions
ExpressionLanguage Symfony Component - https://symfony.com/doc/current/components/expression_language.html
cons - weird array syntax - array['key']. array.key works only for objects
cons - generate notice for array['key'] when key is not defined
pros - stable and well maintainer
https://github.com/mossadal/math-parser
https://github.com/optimistex/math-expression
Please remember that eval is NOT an option, under NO circumstances. We don't live an a static world. Any software always grows and evolves. What was once considered a safe input an one point may turn completely unsafe and uncontrolled.
I think you have a case which can be solved if you model each of your expressions as a rooted directed acyclic graph (DAG).
I assumed acyclic since your ultimate aim is to find the result of boolean algebra operations (if cycling occur in any of graph, then it'd be nonsense I think).
However, even if your graph structure—meaningfully—can be cyclic then your target search term will be cyclic graph, and it should still have a solution.
$expression = '!state1||state4&&(!state2||state5)';
And you have one root with two sub_DAGs in your example.
EXPRESSION as a Rooted DAG:
EXPRESSION
|
AND
___/ \___
OR OR
/ \ / \
! S_1 S_4 ! S_2 S5
Your adjacency list is:
expression_adj_list = [
expression => [ subExp_1, subExp_2 ] ,
subExp_1 => [ ! S_1, S_4 ],
subExp_2 => [ ! S_2, S5 ]
]
Now you can walk through this graph by BFS (breadth-first search algorithm) or DFS (depth-first search algorithm) or your custom, adjusted algorithm.
Of course you can just visit the adjacency list with keys and values as many times as you need if this suits and is easier for you.
You'll need a lookup table to teach your algorithm that. For example,
S2 && S5 = 1,
S1 or S4 = 0,
S3 && S7 = -1 (to throw an exception maybe)
After all, the algorithm below can solve your expression's result.
$adj_list = convert_expression_to_adj_list();
// can also be assigned by a function.
// root is the only node which has no incoming-edge in $adj_list.
$root = 'EXPRESSION';
q[] = $root; //queue to have expression & subexpressions
$results = [];
while ( ! empty(q)) {
$current = array_shift($q);
if ( ! in_array($current, $results)) {
if (isset($adj_list[$current])) { // if has children (sub/expression)
$children = $adj_list[$current];
// true if all children are states. false if any child is subexpression.
$bool = is_calculateable($children);
if ($bool) {
$results[$current] = calc($children);
}
else {
array_unshift($q, $current);
}
foreach ($children as $child) {
if (is_subexpresssion($child) && ! in_array($child, $results)) {
array_unshift($q, $child);
}
}
}
}
}
return $results[$root];
This approach has a great advantage also: if you save the results of the expressions in your database, if an expression is a child of the root expression then you won't need to recalculate it, just use the result from the database for the child subexpressions. In this way, you always have a two-level depth DAG (root and its children).
I'm trying to build multilangual site.
I use this piece of code to detect users language. If you havent chosen a language, it will include your language file based on HTTP_ACCEPT_LANGUAGE.
I don't know where it gets it from though:
session_start();
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
}
elseif (isset($_GET['setLang']) && $_GET['setLang'] == 'en') $_SESSION['lang'] = "en";
elseif (isset($_GET['setLang']) && $_GET['setLang'] == 'sv') $_SESSION['lang'] = "sv";
elseif (isset($_GET['setLang']) && $_GET['setLang'] == 'pl') $_SESSION['lang'] = "pl";
elseif (isset($_GET['setLang']) && $_GET['setLang'] == 'fr') $_SESSION['lang'] = "fr";
include('languages/'.$_SESSION['lang'].'.php');
It works for me and includes the polish lang file. But is this code accurate? Or is there another way?
The browser generally sends a HTTP header, name Accept-Language, that indicates which languages the user is willing to get.
For instance, this header can be :
Accept-Language: en-us,en;q=0.5
There is notion of priority in it, btw ;-)
In PHP, you can get this in the $_SERVER super global :
var_dump($_SERVER['HTTP_ACCEPT_LANGUAGE']);
will get me :
string 'en-us,en;q=0.5' (length=14)
Now, you have to parse that ;-)
If I edit my preferences in the browser's option to say "I want french, and if you can't serve me french, get me english from the US ; and if you can't get me that either, just get me english), the header will be :
Accept-Language: fr-fr,en-us;q=0.7,en;q=0.3
And, from PHP :
string 'fr-fr,en-us;q=0.7,en;q=0.3' (length=26)
For more informations, you can take a look at [section 14.4 of the HTTP RFC][1].
And you probably can find lots of code example in PHP to parse that header ; for instance : Parse Accept-Language to detect a user's language
Have fun !
Here's the script I used for a bi-lingual site. It is to be used as index.php of mysite.com. Based on the user's browser's language preference, it would redirect to desired language version of the site or the default language site if the site in user's preferred langauge was not available.
<?php
// List of available localized versions as 'lang code' => 'url' map
$sites = array(
"en" => "http://en.mysite.com/",
"bn" => "http://bn.mysite.com/",
);
// Get 2 char lang code
$lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
// Set default language if a `$lang` version of site is not available
if (!in_array($lang, array_keys($sites)))
$lang = 'en';
// Finally redirect to desired location
header('Location: ' . $sites[$lang]);
?>
I know there already many good solutions, but have found my own way to solve this problem.
<?php
$prefLocales = array_reduce(
explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']),
function ($res, $el) {
list($l, $q) = array_merge(explode(';q=', $el), [1]);
$res[$l] = (float) $q;
return $res;
}, []);
arsort($prefLocales);
/*
This get you from headers like this
string 'en-US,en;q=0.8,uk;q=0.6,ru;q=0.4' (length=32)
array like this
array (size=4)
'en-US' => float 1
'en' => float 0.8
'uk' => float 0.6
'ru' => float 0.4
*/
Code will convert HTTP_ACCEPT_LANGUAGE string to array with locales as keys and weight as values, sorted from high value to low. So you can just get one by one with array_shift to get the best match with your site locales.
You can use: Locale::acceptFromHttp().
Tries to find locale that can satisfy the language list that is requested by the HTTP "Accept-Language" header.
Your code looks just fine. You might want to add a final else default choice if the visitor asks for a language you aren't providing.
Also, if the visitor himself selects a language you should save that choice in a persistent cookie and check its value, giving it precedence over HTTP_ACCEPT_LANGUAGE.
As far as I can tell Youtube does use HTTP_ACCEPT_LANGUAGE, but at the same time uses IP geolocation to suggest a change in language if the langauge of the visitor's country doesn't match that. Definitely annoying.
Just nitpicking: if you're gonna add languages to the list a switch() statement might be more readable.
Here's a function for selecting the best out of a group of supported languages. It extracts languages from Accept-Language, then sorts the given array of languages according to their priority.
function select_best_language($languages) {
if (!$_SERVER['HTTP_ACCEPT_LANGUAGE']) return $languages[0];
$default_q=100;
foreach (explode(",",$_SERVER['HTTP_ACCEPT_LANGUAGE']) as $lqpair) {
$lq=explode(";q=",$lqpair);
if ($lq[1]) $lq[1]=floatval($lq[1]); else $lq[1]=$default_q--;
$larr[$lq[0]]=$lq[1];
}
usort($languages,function($a,$b) use ($larr) { return $larr[$b]<=>$larr[$a]; });
return $languages[0];
}
$lang = select_best_language(['en','fr','it']);
Try This
function getUserLanguage() {
$langs = array();
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
// break up string into pieces (languages and q factors)
preg_match_all(
'/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
$_SERVER['HTTP_ACCEPT_LANGUAGE'],
$lang_parse
);
if (count($lang_parse[1])) {
// create a list like 'en' => 0.8
$langs = array_combine($lang_parse[1], $lang_parse[4]);
// set default to 1 for any without q factor
foreach ($langs as $lang => $val) {
if ($val === '') {
$langs[$lang] = 1;
}
}
// sort list based on value
arsort($langs, SORT_NUMERIC);
}
}
//extract most important (first)
reset($langs);
$lang = key($langs);
//if complex language simplify it
if (stristr($lang, '-')) {
list($lang) = explode('-', $lang);
}
return $lang;
}
This is also possible. It will use english as default if .php is not available.
$lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
(#include_once 'languages/'.$lang.'.php') or (#include_once 'languages/en.php');
I solved this issue for PHP7.4+ with the code below. It strips out the first two letters of the locale code, takes into account cases like 'en-US' and produces a map like:
$map = [
'en' => 1,
'de' => 0.8,
'uk' => 0.3
];
The code is as follows:
$header = 'en-US,de-DE;q=0.8,uk;q=0.3';
$pattern = '((?P<code>[a-z-_A-Z]{2,5})([;q=]+?(?P<prio>0.\d+))?)';
preg_match_all($pattern, $header, $matches);
['code' => $codes, 'prio' => $values] = $matches;
$map = \array_combine(
\array_map(fn(string $language) => strtolower(substr($language, 0, 2)), \array_values($codes)),
\array_map(fn(string $value) => empty($value) ? 1 : (float)$value, \array_values($values))
);
Please note that the regex is probably suboptimal, improvement suggestions are welcome.
Named capture groups are always in the order of matching, so we can use array_combine safely.
if(isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])){
$parts=explode(';',$_SERVER['HTTP_ACCEPT_LANGUAGE']);
$langs=explode(',',$parts[0]);
var_dump($langs);
}
I'd like to create a php script that runs as a daily cron. What I'd like to do is enumerate through all users within an Active Directory, extract certain fields from each entry, and use this information to update fields within a MySQL database.
Basically what I want to to do is sync up certain user information between Active Directory and a MySQL table.
The problem I have is that the sizelimit on the Active Directory server is often set at 1000 entries per search result. I had hoped that the php function "ldap_next_entry" would get around this by only fetching one entry at a time, but before you can call "ldap_next_entry", you first have to call "ldap_search", which can trigger the SizeLimit exceeded error.
Is there any way besides removing the sizelimit from the server? Can I somehow get "pages" of results?
BTW - I am currently not using any 3rd party libraries or code. Just PHPs ldap methods. Although, I am certainly open to using a library if that will help.
I've been struck by the same problem while developing Zend_Ldap for the Zend Framework. I'll try to explain what the real problem is, but to make it short: until PHP 5.4, it wasn't possible to use paged results from an Active Directory with an unpatched PHP (ext/ldap) version due to limitations in exactly this extension.
Let's try to unravel the whole thing... Microsoft Active Directory uses a so called server control to accomplish server-side result paging. This control ist described in RFC 2696 "LDAP Control Extension for Simple Paged Results Manipulation" .
ext/php offers an access to LDAP control extensions via its ldap_set_option() and the LDAP_OPT_SERVER_CONTROLS and LDAP_OPT_CLIENT_CONTROLS option respectively. To set the paged control you do need the control-oid, which is 1.2.840.113556.1.4.319, and we need to know how to encode the control-value (this is described in the RFC). The value is an octet string wrapping the BER-encoded version of the following SEQUENCE (copied from the RFC):
realSearchControlValue ::= SEQUENCE {
size INTEGER (0..maxInt),
-- requested page size from client
-- result set size estimate from server
cookie OCTET STRING
}
So we can set the appropriate server control prior to executing the LDAP query:
$pageSize = 100;
$pageControl = array(
'oid' => '1.2.840.113556.1.4.319', // the control-oid
'iscritical' => true, // the operation should fail if the server is not able to support this control
'value' => sprintf ("%c%c%c%c%c%c%c", 48, 5, 2, 1, $pageSize, 4, 0) // the required BER-encoded control-value
);
This allows us to send a paged query to the LDAP/AD server. But how do we know if there are more pages to follow and how do we specify with which control-value we have to send our next query?
This is where we're getting stuck... The server responds with a result set that includes the required paging information but PHP lacks a method to retrieve exactly this information from the result set. PHP provides a wrapper for the LDAP API function ldap_parse_result() but the required last parameter serverctrlsp is not exposed to the PHP function, so there is no way to retrieve the required information. A bug report has been filed for this issue but there has been no response since 2005. If the ldap_parse_result() function provided the required parameter, using paged results would work like
$l = ldap_connect('somehost.mydomain.com');
$pageSize = 100;
$pageControl = array(
'oid' => '1.2.840.113556.1.4.319',
'iscritical' => true,
'value' => sprintf ("%c%c%c%c%c%c%c", 48, 5, 2, 1, $pageSize, 4, 0)
);
$controls = array($pageControl);
ldap_set_option($l, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_bind($l, 'CN=bind-user,OU=my-users,DC=mydomain,DC=com', 'bind-user-password');
$continue = true;
while ($continue) {
ldap_set_option($l, LDAP_OPT_SERVER_CONTROLS, $controls);
$sr = ldap_search($l, 'OU=some-ou,DC=mydomain,DC=com', 'cn=*', array('sAMAccountName'), null, null, null, null);
ldap_parse_result ($l, $sr, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls); // (*)
if (isset($serverctrls)) {
foreach ($serverctrls as $i) {
if ($i["oid"] == '1.2.840.113556.1.4.319') {
$i["value"]{8} = chr($pageSize);
$i["iscritical"] = true;
$controls = array($i);
break;
}
}
}
$info = ldap_get_entries($l, $sr);
if ($info["count"] < $pageSize) {
$continue = false;
}
for ($entry = ldap_first_entry($l, $sr); $entry != false; $entry = ldap_next_entry($l, $entry)) {
$dn = ldap_get_dn($l, $entry);
}
}
As you see there is a single line of code (*) that renders the whole thing useless. On my way though the sparse information on this subject I found a patch against the PHP 4.3.10 ext/ldap by Iñaki Arenaza but neither did I try it nor do I know if the patch can be applied on a PHP5 ext/ldap. The patch extends ldap_parse_result() to expose the 7th parameter to PHP:
--- ldap.c 2004-06-01 23:05:33.000000000 +0200
+++ /usr/src/php4/php4-4.3.10/ext/ldap/ldap.c 2005-09-03 17:02:03.000000000 +0200
## -74,7 +74,7 ##
ZEND_DECLARE_MODULE_GLOBALS(ldap)
static unsigned char third_argument_force_ref[] = { 3, BYREF_NONE, BYREF_NONE, BYREF_FORCE };
-static unsigned char arg3to6of6_force_ref[] = { 6, BYREF_NONE, BYREF_NONE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE };
+static unsigned char arg3to7of7_force_ref[] = { 7, BYREF_NONE, BYREF_NONE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE };
static int le_link, le_result, le_result_entry, le_ber_entry;
## -124,7 +124,7 ##
#if ( LDAP_API_VERSION > 2000 ) || HAVE_NSLDAP
PHP_FE(ldap_get_option, third_argument_force_ref)
PHP_FE(ldap_set_option, NULL)
- PHP_FE(ldap_parse_result, arg3to6of6_force_ref)
+ PHP_FE(ldap_parse_result, arg3to7of7_force_ref)
PHP_FE(ldap_first_reference, NULL)
PHP_FE(ldap_next_reference, NULL)
#ifdef HAVE_LDAP_PARSE_REFERENCE
## -1775,14 +1775,15 ##
Extract information from result */
PHP_FUNCTION(ldap_parse_result)
{
- pval **link, **result, **errcode, **matcheddn, **errmsg, **referrals;
+ pval **link, **result, **errcode, **matcheddn, **errmsg, **referrals, **serverctrls;
ldap_linkdata *ld;
LDAPMessage *ldap_result;
+ LDAPControl **lserverctrls, **ctrlp, *ctrl;
char **lreferrals, **refp;
char *lmatcheddn, *lerrmsg;
int rc, lerrcode, myargcount = ZEND_NUM_ARGS();
- if (myargcount 6 || zend_get_parameters_ex(myargcount, &link, &result, &errcode, &matcheddn, &errmsg, &referrals) == FAILURE) {
+ if (myargcount 7 || zend_get_parameters_ex(myargcount, &link, &result, &errcode, &matcheddn, &errmsg, &referrals, &serverctrls) == FAILURE) {
WRONG_PARAM_COUNT;
}
## -1793,7 +1794,7 ##
myargcount > 3 ? &lmatcheddn : NULL,
myargcount > 4 ? &lerrmsg : NULL,
myargcount > 5 ? &lreferrals : NULL,
- NULL /* &serverctrls */,
+ myargcount > 6 ? &lserverctrls : NULL,
0 );
if (rc != LDAP_SUCCESS ) {
php_error(E_WARNING, "%s(): Unable to parse result: %s", get_active_function_name(TSRMLS_C), ldap_err2string(rc));
## -1805,6 +1806,29 ##
/* Reverse -> fall through */
switch(myargcount) {
+ case 7 :
+ zval_dtor(*serverctrls);
+
+ if (lserverctrls != NULL) {
+ array_init(*serverctrls);
+ ctrlp = lserverctrls;
+
+ while (*ctrlp != NULL) {
+ zval *ctrl_array;
+
+ ctrl = *ctrlp;
+ MAKE_STD_ZVAL(ctrl_array);
+ array_init(ctrl_array);
+
+ add_assoc_string(ctrl_array, "oid", ctrl->ldctl_oid,1);
+ add_assoc_bool(ctrl_array, "iscritical", ctrl->ldctl_iscritical);
+ add_assoc_stringl(ctrl_array, "value", ctrl->ldctl_value.bv_val,
+ ctrl->ldctl_value.bv_len,1);
+ add_next_index_zval (*serverctrls, ctrl_array);
+ ctrlp++;
+ }
+ ldap_controls_free (lserverctrls);
+ }
case 6 :
zval_dtor(*referrals);
if (array_init(*referrals) == FAILURE) {
Actually the only option left would be to change the Active Directory configuration and raise the maximum result limit. The relevant option is called MaxPageSize and can be altered by using ntdsutil.exe - please see "How to view and set LDAP policy in Active Directory by using Ntdsutil.exe".
EDIT (reference to COM):
Or you can go the other way round and use the COM-approach via ADODB as suggested in the link provided by eykanal.
Support for paged results was added in PHP 5.4.
See ldap_control_paged_result for more details.
This isn't a full answer, but this guy was able to do it. I don't understand what he did, though.
By the way, a partial answer is that you CAN get "pages" of results. From the documentation:
resource ldap_search ( resource $link_identifier , string $base_dn ,
string $filter [, array $attributes [, int $attrsonly [, int $sizelimit [,
int $timelimit [, int $deref ]]]]] )
...
sizelimit Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
Note: This parameter can NOT override server-side preset sizelimit.
You can set it lower though. Some directory server hosts will be
configured to return no more than a preset number of entries. If this
occurs, the server will indicate that it has only returned a partial
results set. This also occurs if you use this parameter to limit the
count of fetched entries.
I don't know how to specify that you want to search STARTING from a certain position, though. I.e., after you get your first 1000, I don't know how to specify that now you need the next 1000. Hopefully someone else can help you there :)
Here's an alternative (which works pre PHP 5.4). If you have 10,000 records you need to get but your AD server only returns 5,000 per page:
$ldapSearch = ldap_search($ldapResource, $basedn, $filter, array('member;range=0-4999'));
$ldapResults = ldap_get_entries($dn, $ldapSearch);
$members = $ldapResults[0]['member;range=0-4999'];
$ldapSearch = ldap_search($ldapResource, $basedn, $filter, array('member;range=5000-10000'));
$ldapResults = ldap_get_entries($dn, $ldapSearch);
$members = array_merge($members, $ldapResults[0]['member;range=5000-*']);
I was able to get around the size limitation using ldap_control_paged_result
ldap_control_paged_result is used to Enable LDAP pagination by sending the pagination control. The below function worked perfectly in my case.
function retrieves_users($conn)
{
$dn = 'ou=,dc=,dc=';
$filter = "(&(objectClass=user)(objectCategory=person)(sn=*))";
$justthese = array();
// enable pagination with a page size of 100.
$pageSize = 100;
$cookie = '';
do {
ldap_control_paged_result($conn, $pageSize, true, $cookie);
$result = ldap_search($conn, $dn, $filter, $justthese);
$entries = ldap_get_entries($conn, $result);
if(!empty($entries)){
for ($i = 0; $i < $entries["count"]; $i++) {
$data['usersLdap'][] = array(
'name' => $entries[$i]["cn"][0],
'username' => $entries[$i]["userprincipalname"][0]
);
}
}
ldap_control_paged_result_response($conn, $result, $cookie);
} while($cookie !== null && $cookie != '');
return $data;
}