PHP - Displaying random output more than once, prevent duplicate - php

I have the following code that I am using to randomly display PHP widgets from a folder:
<?php
function random_widget($dir = 'wp-content/themes/zonza/elements')
{
$files = glob($dir . '/*.*');
$file = array_rand($files);
return $files[$file];
}
?>
<?php include random_widget();?>
<?php include random_widget();?>
<?php include random_widget();?>
random_widget(); outputs a URL, which I then use in the include function to display the widget.
The code randomly chooses between 6 php files and displays one randomly. I include it 3 times to get 3 widgets. However, I get the same widget displayed more than once sometimes.
What can I do to modify the code to prevent this from happening?

Try this:
<?php
function random_widget($dir = 'wp-content/themes/zonza/elements')
{
static $files = false;
if(!$files) $files=glob($dir . '/*.*');
$key = array_rand($files);
$file=$files[$key];
unset($files[$key]);
return $file;
}
?>
It works by removing the file returned from $files, and maintaining $files over multiple function calls (it only globs() on the first time you call the function)

Declare files at the beginning of the page $files = glob($dir . '/*.*');
In random_widget, do this unset($files[$file]) after you pull the value.

array_rand takes a number $num_req as an optional second parameter which specifies the number of entries you want to pick. So add this parameter to random_widget, pass it to array_rand to get an array of keys instead of a single key, return the array of files, and then iterate over this array to include the widgets (instead of calling random_widget three times).
<?php
function random_widget($num_req, $dir = 'wp-content/themes/zonza/elements')
{
$files = glob($dir . '/*.*');
$keys = array_rand($files, $num_req);
$chosen = array();
foreach($keys as $key) {
$chosen[] = $files[$key];
}
return $chosen;
}
$widgets = random_widget(3);
foreach($widgets as $widget) {
include $widget;
}
?>
An advantage of this solution over the ones proposed in the other answers is that it is stateless: you can reuse the function in different contexts as much as you want.
Source: http://php.net/manual/en/function.array-rand.php

Related

PHP 5.3.x - How do I turn the server path into its domain name, and a clickable URL?

I'm a newbie...sorry...I'll admit that I've cobbled this script together from several sources, but I'm trying to learn. :-) Thanks for any help offered!!
$directory = new \RecursiveDirectoryIterator(__DIR__, \FilesystemIterator::FOLLOW_SYMLINKS);
$filter = new \RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) {
if ($current->getFilename() === '.') {
return FALSE;
}
if ($current->isDir()) {
return $current->getFilename() !== 'css';
}
else {
// Only consume files of interest.
return strpos($current->getFilename(), 'story.html') === 0;
}
});
$iterator = new \RecursiveIteratorIterator($filter);
$files = array();
foreach ($iterator as $info) {
$files[] = $info->getPathname();
}
?>
Then down in my HTML is where I run into problems, in the 2nd echo statement...
<?php
echo '<ul>';
foreach ($files as $item){
echo '<li>http://<domain.com/directory/subdirectory/story.html></li>';
echo '</ul>';
};
?>
The purpose of my script is to "crawl" a directory looking for a specific file name in sub-directories. Then, when it finds this file, to create a human-readable, clickable URL from the server path. Up to now, my HTML gets one of these two server paths as my list item:
http://thedomain.com/var/www/vhosts/thedomain.com/httpdocs/directory/subdirectory/story.html
or
file:///C:/Bitnami/wampstack-5.5.30-0/apache2/htdocs/directory/subdirectory/story.html
...depending on where I'm running my .php page.
I feel like I need to "strip away" part of these paths... to get down to /subdirectory/story.html ... If I could do that, then I think I can add the rest into my echo statements. Everything I've found for stripping strings has been from the trailing end of the path, not the leading end. (dirname($item)) takes away the filename, and (basename($item)) takes away the subdirectory and the filename ... the bits I want!!
Try this function
function strip($url){
$info = parse_url($url);
$slash = (explode('/',$info['path']));
$sub = $slash[count($slash)-2];
$file = basename($url)==$sub ? "" : basename($url);
return "/".$sub."/".$file;
}
calling it by
echo strip('file:///C:/Bitnami/wampstack-5.5.30-0/apache2/htdocs/directory/subdirectory/story.html');
will result in
/subdirectory/story.html

Delete files not matching a list

So I'm trying to make a simple script, it will have a list of predefined files, search for anything that's not on the list and delete it.
I have this for now
<?php
$directory = "/home/user/public_html";
$files = glob($directory . "*.*");
foreach($files as $file)
{
$sql = mysql_query("SELECT id FROM files WHERE FileName='$file'");
if(mysql_num_rows($sql) == 0)
unlink($directory . $file);
}
?>
However, I'd like to avoid the query so I can run the script more often (there's about 60-70 files, and I want to run this every 20 seconds or so?) so how would I embedd a file list into the php file and check against that instead of database?
Thanks!
You are missing a trailing / twice.. In glob() you are giving /home/user/public_html*.* as the argument, I think you mean /home/user/public_html/*.*.
This is why I bet nothing matches the files in your table..
This won't give an error either because the syntax is fine.
Then where you unlink() you do this again.. your argument home/user/public_htmltestfile.html should be home/user/public_html/testfile.html.
I like this syntax style: "{$directory}/{$file}" because it's short and more readable. If the / is missing, you see it immediately. You can also change it to $directory . "/" . $file, it you prefer it. The same goes for one line conditional statements.. So here it comes..
<?php
$directory = "/home/user/public_html";
$files = glob("{$directory}/*.*");
foreach($files as $file)
{
$sql = mysql_query("SELECT id FROM files WHERE FileName=\"{$file}\";");
if(mysql_num_rows($sql) == 0)
{
unlink("{$directory}/{$file}");
}
}
?>
EDIT: You requested recursion. Here it goes..
You need to make a function that you can run once with a path as it's argument. Then you can run that function from inside that function on subdirectories. Like this:
<?php
/*
ListDir list files under directories recursively
Arguments:
$dir = directory to be scanned
$recursive = in how many levels of recursion do you want to search? (0 for none), default: -1 (for "unlimited")
*/
function ListDir($dir, $recursive=-1)
{
// if recursive == -1 do "unlimited" but that's no good on a live server! so let's say 999 is enough..
$recursive = ($recursive == -1 ? 999 : $recursive);
// array to hold return value
$retval = array();
// remove trailing / if it is there and then add it, to make sure there is always just 1 /
$dir = rtrim($dir,"/") . "/*";
// read the directory contents and process each node
foreach(glob($dir) as $node)
{
// skip hidden files
if(substr($node,-1) == ".") continue;
// if $node is a dir and recursive is greater than 0 (meaning not at the last level or disabled)
if(is_dir($node) && $recursive > 0)
{
// substract 1 of recursive for ever recursion.
$recursive--;
// run this same function again on itself, merging the return values with the return array
$retval = array_merge($retval, ListDir($node, $recursive));
}
// if $node is a file, we add it to the array that will be returned from this function
elseif(is_file($node))
{
$retval[] = $node;
// NOTE: if you want you can do some action here in your case you can unlink($node) if it matches your requirements..
}
}
return $retval;
}
// Output the result
echo "<pre>";
print_r(ListDir("/path/to/dir/",1));
echo "</pre>";
?>
If the list is not dynamic, store it in an array:
$myFiles = array (
'some.ext',
'next.ext',
'more.ext'
);
$directory = "/home/user/public_html/";
$files = glob($directory . "*.*");
foreach($files as $file)
{
if (!in_array($file, $myFiles)) {
unlink($directory . $file);
}
}

How can I exclude directories using RecursiveDirectoryIterator

I have the function below. Which goes through directories and recursively searches through them to grab a random image file and then attaches that to a post. What I want to do is exclude some files from the search.
I have a comma separated list which I explode into an array, I tried using a filter but couldn't get this to work.
Current function without filter is
function swmc_get_imgs($start_dir, $ext, $exclude=array()){
$dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($start_dir));
$files = array();
// Force array of extensions and make them all lower-case
if ( ! is_array($ext))
{
$ext = (array) $ext;
}
$ext = array_unique(array_map('strtolower', $ext));
foreach($dir as $file)
{
// Skip anything that isn't a file
if ( ! $file->isFile())
continue;
// If the file has one of our desired extensions, add it to files array
if (in_array(strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION)), $ext)) {
$files[] = $file->getPathname();
}
}
return $files;
}
So the above works but can be fairly expensive still especially with a lot of directories, as such I want to exclude a list of directories stored in a comma list.
I tried the following
class SwmcOnlyFilter extends RecursiveFilterIterator {
public function accept() {
// Accept the current item if we can recurse into it
// or it is a value starting with "test"
return $this->hasChildren() || !in_array($this->current(), explode(",",get_option('swmc_image_excl')));
}
}
And then changing the first part of the swmc_get_imgs function to
$dirIterator = new RecursiveDirectoryIterator($start_dir);
$filter = new SwmcOnlyFilter($dirIterator);
$dir = new RecursiveIteratorIterator($filter);
However the filter doesn't jump over that directory but instead goes into it.
The directories could look like
/uploads/2009/1/2011_pic.jpg
/uploads/2011/1/john_smith.jpg
and so on.
So I may want to exclude 2011 as a directory but not exclude the image that lives in 2009 with 2011 in its title.
CLARIFICATION:
I could filter out these manually by skipping them in the foreach loop, however this still checks them and wastes memory and time. I would prefer to skip these at the time of the grab if possible.
figured it out using the following
function swmc_iterate_imgs($start_dir) {
$directory = $start_dir;
$excludedDirs = explode(",",get_option('swmc_image_excl')); // array of subdirectory paths, relative to $directory, to exclude from search
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
$fileArr = array(); // numerically indexed array with your files
$x = -1;
while ($it->valid())
{
if (!$it->isDot() && !in_array($it->getSubPath(), $excludedDirs) && preg_match('/(\.(jpg|jpeg|gif|png))$/i', $it->key()) == 1)
{
$fileArr[] = $it->key();
}
$it->next();
}
return $fileArr;
}

How to get the newest file in a directory in php

So I have this app that processes CSV files. I have a line of code to load the file.
$myFile = "data/FrontlineSMS_Message_Export_20120721.csv"; //The name of the CSV file
$fh = fopen($myFile, 'r'); //Open the file
I would like to find a way in which I could look in the data directory and get the newest file (they all have date tags so they would be in order inside of data) and set the name equal to $myFile.
I really couldn't find and understand the documentation of php directories so any helpful resources would be appreciated as well. Thank you.
Here's an attempt using scandir, assuming the only files in the directory have timestamped filenames:
$files = scandir('data', SCANDIR_SORT_DESCENDING);
$newest_file = $files[0];
We first list all files in the directory in descending order, then, whichever one is first in that list has the "greatest" filename — and therefore the greatest timestamp value — and is therefore the newest.
Note that scandir was added in PHP 5, but its documentation page shows how to implement that behavior in PHP 4.
For a search with wildcard you can use:
<?php
$path = "/var/www/html/*";
$latest_ctime = 0;
$latest_filename = '';
$files = glob($path);
foreach($files as $file)
{
if (is_file($file) && filectime($file) > $latest_ctime)
{
$latest_ctime = filectime($file);
$latest_filename = $file;
}
}
return $latest_filename;
?>
My solution, improved solution from Max Hofmann:
$ret = [];
$dir = Yii::getAlias("#app") . "/web/uploads/problem-letters/{$this->id}"; // set directory in question
if(is_dir($dir)) {
$ret = array_diff(scandir($dir), array(".", "..")); // get all files in dir as array and remove . and .. from it
}
usort($ret, function ($a, $b) use ($dir) {
if(filectime($dir . "/" . $a) < filectime($dir . "/" . $b)) {
return -1;
} else if(filectime($dir . "/" . $a) == filectime($dir . "/" . $b)) {
return 0;
} else {
return 1;
}
}); // sort array by file creation time, older first
echo $ret[count($ret)-1]; // filename of last created file
Here's an example where I felt more confident in using my own validator rather than simply relying on a timestamp with scandir().
In this context, I want to check if my server has a more recent file version than the client's version. So I compare version numbers from the file names.
$clientAppVersion = "1.0.5";
$latestVersionFileName = "";
$directory = "../../download/updates/darwin/"
$arrayOfFiles = scandir($directory);
foreach ($arrayOfFiles as $file) {
if (is_file($directory . $file)) {
// Your custom code here... For example:
$serverFileVersion = getVersionNumberFromFileName($file);
if (isVersionNumberGreater($serverFileVersion, $clientAppVersion)) {
$latestVersionFileName = $file;
}
}
}
// function declarations in my php file (used in the forEach loop)
function getVersionNumberFromFileName($fileName) {
// extract the version number with regEx replacement
return preg_replace("/Finance D - Tenue de livres-darwin-(x64|arm64)-|\.zip/", "", $fileName);
}
function removeAllNonDigits($semanticVersionString) {
// use regex replacement to keep only numeric values in the semantic version string
return preg_replace("/\D+/", "", $semanticVersionString);
}
function isVersionNumberGreater($serverFileVersion, $clientFileVersion): bool {
// receives two semantic versions (1.0.4) and compares their numeric value (104)
// true when server version is greater than client version (105 > 104)
return removeAllNonDigits($serverFileVersion) > removeAllNonDigits($clientFileVersion);
}
Using this manual comparison instead of a timestamp I can achieve a more surgical result. I hope this can give you some useful ideas if you have a similar requirement.
(PS: I took time to post because I was not satisfied with the answers I found relating to the specific requirement I had. Please be kind I'm also not very used to StackOverflow - Thanks!)

Random file from folder (no repetition)

So I'm using the following code to pull a random file from a folder and I would like to make it so there is never a chance of pulling up the current file again (ie: seeing the same image/document twice in a row).
How would I go about this? Thanks in advance!
function random_file($dir = 'destinations')
{
$files = glob($dir . '/*.*');
$file = array_rand($files);
return $files[$file];
}
Store the last viewed filename in a cookie or in the session.
Here's how to do it with a cookie:
function random_file($dir = 'destinations') {
$files = glob($dir . '/*.*');
if (!$files) return false;
$files = array_diff($files, array(#$_COOKIE['last_file']));
$file = array_rand($files);
setcookie('last_file', $files[$file]);
return $files[$file];
}
$picker = new FilePicker();
$picker->randomFile();
$picker->randomFile(); // never the same as the previous
--
class FilePicker
{
private $lastFile;
public function randomFile($dir = 'destinations')
{
$files = glob($dir . '/*.*');
do {
$file = array_rand($files);
} while ($this->lastFile == $file);
$this->lastFile = $file;
return $files[$file];
}
}
Essentially: store the name of every file used in an array; every time you pull a new name, check whether it already exists in the array.
in_array() will help you with that. array_push() will help fill the "used files" array.
You could make the array a static one to have the list available whenever you call the function (instead of using global variables).
If you want to present a fixed set of files in random order,
then read all file names into an array, shuffle the array
and then just use the array from start to end.

Categories