Using PHP to read from RSS and post to Mastodon

I am currently using a version of the following PHP code to automate the posting of content from World War II Database to the Mastodon social network. First, please note that you will need the following https://mastodon.social/ information that you can set up after logging on to your Mastodon account.

  • Client ID
  • Client Secret
  • Access Token

There are two more things left to configure.

  • URL to the RSS Feed
  • File Path/Name for a Log File – This is used to avoid posting duplicate content on Mastodon

Below is the code. Note that in Step 2 there is a hard-coded value of “100” to note that the log file (to keep track of which RSS content has already been posted) will keep track of the most recent 100 records; Step 4 posts in the format of title-space-link, which you may wish to adjust based on your RSS feed structure.

$clientId = 'client_id_here';
$clientSecret = 'client_secret_here';
$accessToken = 'access_token_here';
$redirectUri = 'urn:ietf:wg:oauth:2.0:oob';
$rssUrl = 'rss_feed_url_here';
$postedIdsFile = 'rss_to_mastodon_posted_ids.txt';

// Change the following to "N" to output some debugging information
$silentMode = "Y";

// Step 1: Establish a function to fetch RSS feed
function fetchRss($rssUrl) {
    $rss = simplexml_load_file($rssUrl);
    return $rss;
}

// Step 2: Establish a function to check the log file to avoid duplicates
function checkAndUpdatePostIds($postId, $file) {
    $postedIds = file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES) : [];
    if (in_array($postId, $postedIds)) {
        return false;
    }
    array_push($postedIds, $postId);
    if (count($postedIds) > 100) {
        array_shift($postedIds);
    }
    file_put_contents($file, implode("\n", $postedIds) . "\n");
    return true;
}

// Step 3: Establish a function to post to Mastodon
function postToMastodon($accessToken, $content) {
    $url = "https://mastodon.social/api/v1/statuses";

    $data = [
        'status' => $content,
    ];

    $options = [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($data),
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer $accessToken",
            "Content-Type: application/x-www-form-urlencoded",
        ],
    ];

    $ch = curl_init();
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    curl_close($ch);

    return $response;
}

// Step 4: Put eveything together
if (!$accessToken) {
    die("Error: Unable to obtain access token. Exiting...");
}

$rss = fetchRss($rssUrl);

foreach ($rss->channel->item as $item) {
    $postId = (string) $item->guid;
    $content = (string) $item->title . " " . (string) $item->link;

    if (checkAndUpdatePostIds($postId, $postedIdsFile)) {
        if ($silentMode == "N") {
            echo "Posting: $content\n";
        }
        $response = postToMastodon($accessToken, $content);
        if ($silentMode == "N") {
            echo $response . "\n";
        }
    } else {
        if ($silentMode == "N") {
            echo "Duplicate detected, skipping: $content\n";
        }
    }
}

As far as usage goes, you can refactor the various pieces to fit into your existing PHP-based management tool. As a shortcut, you can also take the above code as-is and run it via cron or other similar job schedulers.

My implementation of this code posts contents to the WW2DB Mastodon page at the URL https://mastodon.social/@ww2db.

Using PHP to read from RSS and post to Bluesky

I am currently using a version of the following PHP code to automate the posting of content from World War II Database to the Bluesky social network. First, please note that you will need the following Bluesky information.

  • Bluesky Username – Likely the email address you use to log in to Bluesky
  • Password
  • Repository Name – Likely your Bluesky name followed by “bsky.social”; for example, “ww2db.bsky.social”

There are two more things left to configure.

  • URL to the RSS Feed
  • File Path/Name for a Log File – This is used to avoid posting duplicate content on Bluesky

Below is the code. Note that in Step 3 there is a hard-coded value of “100” to note that the log file (to keep track of which RSS content has already been posted) will keep track of the most recent 100 records; in Step 4 there is some code to handle hashtags and URLs that may be present in the RSS content; Step 5 contains the code to obtain an access token which will be used for the actual posting; finally, Step 6 posts in the format of title-space-link, which you may wish to adjust based on your RSS feed structure.

$username = 'username_here';
$password = 'password_here';
$repoName = 'repo_name_here';
$rssUrl = 'rss_url_here';
$postedIdsFile = 'rss_to_bksy_posted_ids.txt';

// Change the following to "N" to output some debugging information
$silentMode = "Y";

// Step 1: Establish a function to get Bluesky access token
function getAccessToken($username, $password) {
    $url = 'https://bsky.social/xrpc/com.atproto.server.createSession';
    $data = [
        'identifier' => $username,
        'password' => $password
    ];

    $options = [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($data),
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json"
        ]
    ];

    $ch = curl_init();
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    curl_close($ch);

    $response_data = json_decode($response, true);
    return $response_data['accessJwt'];
}

// Step 2: Establish a function to fetch content from a RSS feed
function fetchRss($rssUrl) {
    $rss = simplexml_load_file($rssUrl);
    return $rss;
}

// Step 3: Establish a function to check if post ID exists in the log file, and to update the log file
function checkAndUpdatePostIds($postId, $file) {
    $postedIds = file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES) : [];

    if (in_array($postId, $postedIds)) {
        return false;
    }

    array_push($postedIds, $postId);
    if (count($postedIds) > 100) {
        array_shift($postedIds);
    }
    file_put_contents($file, implode("\n", $postedIds) . "\n");
    return true;
}

// Step 4: Establish a function to handle Bluesky facets for hashtags and URLs
function extractFacets($content) {
    preg_match_all('/#(\w+)/', $content, $hashtags);
    preg_match_all('/https?:\/\/[^\s]+/', $content, $urls);

    $facets = [];

    foreach ($hashtags[0] as $hashtag) {
        $startPos = strpos($content, $hashtag);
        $facets[] = [
            'index' => [
                'byteStart' => $startPos,
                'byteEnd' => $startPos + strlen($hashtag)
            ],
            'features' => [
                [
                    '$type' => 'app.bsky.richtext.facet#tag',
                    'tag' => $hashtag
                ]
            ]
        ];
    }

    foreach ($urls[0] as $url) {
        $startPos = strpos($content, $url);
        $facets[] = [
            'index' => [
                'byteStart' => $startPos,
                'byteEnd' => $startPos + strlen($url)
            ],
            'features' => [
                [
                    '$type' => 'app.bsky.richtext.facet#link',
                    'uri' => $url
                ]
            ]
        ];
    }

    return $facets;
}

// Step 5: Establish a function to post to Bluesky
function postToBluesky($accessJwt, $repoName, $content) {
    $facets = extractFacets($content);

    $url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord';
    $data = [
        'repo' => $repoName,
        'collection' => 'app.bsky.feed.post',
        'record' => [
            'text' => $content,
            'createdAt' => date('c'),
            'facets' => $facets
        ]
    ];

    $options = [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($data),
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json",
            "Authorization: Bearer $accessJwt"
        ]
    ];

    $ch = curl_init();
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    curl_close($ch);

    return $response;
}

// Step 6: Put everything together

$accessJwt = getAccessToken($username, $password);
$rss = fetchRss($rssUrl);

foreach ($rss->channel->item as $item) {
    $postId = (string) $item->guid;
    $content = (string) $item->title . " " . (string) $item->link;

    if (checkAndUpdatePostIds($postId, $postedIdsFile)) {
        if ($silentMode == "N") {
			echo "Posting: $content\n";
		}
        $response = postToBluesky($accessJwt, $repoName, $content);
		if ($silentMode == "N") {
			echo $response . "\n";
		}
    }
	else {
		if ($silentMode == "N") {
			echo "Duplicate post detected, skipping: $content\n";
		}
    }
}

As far as usage goes, you can refactor the various pieces to fit into your existing PHP-based management tool. As a shortcut, you can also take the above code as-is and run it via cron or other similar job schedulers.

My implementation of this code posts contents to the WW2DB Bluesky page at the URL https://bsky.app/profile/ww2db.bsky.social.

Using PHP to read from RSS and post to Reddit

I am currently using a version of the following PHP code to automate the posting of content from World War II Database to the Reddit social network. First, please note that you will need the following Reddit information that you can set up after logging on to your Reddit account. You will also need to establish an app at https://www.reddit.com/prefs/apps, and the User Agent name you will set up will also be part of the configuration. And of course, you will need to enter the name of your own subreddit to post to; if you are posting to someone else’s subreddit, be sure to obey that particular community’s rules.

  • Client ID
  • Client Secret
  • Username
  • Password
  • User Agent Name

There are two more things left to configure.

  • URL to the RSS Feed
  • File Path/Name for a Log File – This is used to avoid posting duplicate content on Reddit

Below is the code. Note that in Step 3 there is a hard-coded value of “100” to note that the log file (to keep track of which RSS content has already been posted) will keep track of the most recent 100 records; Steps 4 and 5 makes the posts in the format of a “link” type Reddit post, which you may wish to adjust based on your needs.

$clientId = 'client_id_here';
$clientSecret = 'client_secret_here';
$username = 'username_here';
$password = 'password_here';
$userAgent = 'user_agent_here';
$rssUrl = 'rss_feed_url_here';
$subreddit = 'r/subreddit_name';
$postedIdsFile = 'rss_to_reddit_posted_ids.txt';

$silentMode = "Y";

// Step 1: Establish a function to fetch an access token
function getAccessToken($clientId, $clientSecret, $username, $password, $userAgent) {
    $url = 'https://www.reddit.com/api/v1/access_token';
    $postFields = http_build_query([
        'grant_type' => 'password',
        'username' => $username,
        'password' => $password
    ]);

    $options = [
        CURLOPT_URL => $url,
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERPWD => "$clientId:$clientSecret",
        CURLOPT_POSTFIELDS => $postFields,
        CURLOPT_HTTPHEADER => ["User-Agent: $userAgent"]
    ];

    $ch = curl_init();
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    curl_close($ch);

    $response_data = json_decode($response, true);
    return $response_data['access_token'] ?? null;
}

// Step 2: Establish a function to fetch content from the RSS feed
function fetchRss($rssUrl) {
    return simplexml_load_file($rssUrl);
}

// Step 3: Establish a function to track and avoid duplicate posts
function checkAndUpdatePostIds($postId, $file) {
    $postedIds = file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES) : [];
    if (in_array($postId, $postedIds)) {
        return false;
    }
    array_push($postedIds, $postId);
    if (count($postedIds) > 100) {
        array_shift($postedIds); // Keep the file small
    }
    file_put_contents($file, implode("\n", $postedIds) . "\n");
    return true;
}

// Step 4: Establish a function to post to Reddit
function postToReddit($accessToken, $subreddit, $title, $url, $userAgent) {
    $apiUrl = "https://oauth.reddit.com/api/submit";
    $postData = [
        'sr' => $subreddit,
        'title' => $title,
        'url' => $url,
        'kind' => 'link'
    ];

    $options = [
        CURLOPT_URL => $apiUrl,
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => http_build_query($postData),
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer $accessToken",
            "User-Agent: $userAgent",
            "Content-Type: application/x-www-form-urlencoded"
        ]
    ];

    $ch = curl_init();
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    curl_close($ch);

    return json_decode($response, true);
}

// Step 5: Put it all together
$accessToken = getAccessToken($clientId, $clientSecret, $username, $password, $userAgent);
if (!$accessToken) {
    die("Error: Unable to obtain access token. Exiting...\n");
}

$rss = fetchRss($rssUrl);
if (!$rss) {
    die("Error: Unable to fetch RSS feed. Exiting...\n");
}

foreach ($rss->channel->item as $item) {
    $postId = (string) $item->guid;
    $title = (string) $item->title;
    $url = (string) $item->link;

    if (checkAndUpdatePostIds($postId, $postedIdsFile)) {
        if ($silentMode == "N") {
            echo "Posting to Reddit: $title ($url)\n";
        }
        $response = postToReddit($accessToken, $subreddit, $title, $url, $userAgent);
        if ($silentMode == "N") {
            echo "Reddit Response: " . json_encode($response) . "\n";
        }
    } else {
        if ($silentMode == "N") {
            echo "Duplicate detected, skipping: $title ($url)\n";
        }
    }
}

As far as usage goes, you can refactor the various pieces to fit into your existing PHP-based management tool. As a shortcut, you can also take the above code as-is and run it via cron or other similar job schedulers.

My implementation of this code posts contents to the WW2DB subreddit at the URL https://www.reddit.com/r/ww2database/.

Audit Unsuccessful Oracle Login Attempts

Recently, the corporate parent of a client began dictating regular password changes for various Oracle service accounts, ie. accounts that the developers with the client as well as myself had been using to run automated tasks, automated reports, etc. After the password was changed, most of those jobs were documented and thus easy to find and update, but almost immediately we noticed that one of the service accounts began locking up every few minutes, so we obviously missed at least one, and it was too early in the morning for us to have consumed enough cups of coffee to remember what we may have missed.

The built-in DBA_AUDIT_TRAIL view came to our rescue.

The following command enables the logging of failed login attempts.

audit session whenever not successful;

Once that was enabled, we simply waited a few minutes, and then ran this SQL below.

select os_username, username, terminal, returncode, to_char(timestamp,'mm/dd/yyyy hh24:mi:ss') as fail_time, comment_text
from dba_audit_trail
where username='ACCOUNT_USERNAME' 
and returncode in(1017,28000)
order by timestamp desc;

VoilĂ ! We found our culprit. The field “terminal” showed us the machine’s hostname, and its IP address was found in the “comment_text” field. Return code 1017 signifies a login failure due to bad password, and 28000 is a login failure due to locked account.

Parsing Apache Logs with tail, cut, sort, and uniq

A client experienced some intermittent website down time last week during the final few days of April 2021, and sent over that month’s Apache logs for me to see if there is anything out of the ordinary – excessive crawling, excessive probing, brute force password attacks, things of that nature. Below are a few commands I have used that I thought would be nice to keep handy for future uses. I am currently using Ubuntu 20.04 LTS.

While unrelated, just to form a complete picture, my client sent the logs to me in gz-compressed format. If you are not familiar on how to uncompress it, it is fairly straight forward:

gunzip 2021-APR.gz

Back on topic… I ended on parsing the file in three separate ways for me to get an overall view of things. I found that the final few days of April are represented in the roughly final 15,000 lines of the log file, so I decided to use the tail command as my main tool.

First, I did following command below to find which IP addresses hit the server the most:

tail -n 15000 filename.log | cut -f 1 -d ' ' | sort | uniq -c | sort -nr | more

Quick explanation:

  • The tail command pulls the final 15,000 lines from the log file (final few days of the month)
  • The cut command parses each line using a space delimiter and returns the first field (the IP addres)
  • The sort command sorts the results thus far
  • The uniq command groups the results thus far and provides a count
  • The second sort command reverse the sort so the highest result is on top
  • Finally, the more command creates screen-sized pagination so it’s easier to read

There is always more than one way to do something in Linux, of course. Just as an aside, the following functions very similarly:

cat filename.log | awk '{print $1}' | sort | uniq -c | sort -nr | tail -15000 | more

Then, I thought it would be nice to get an idea of how many requests were made per hour. This can be achieved with the command below.

tail -n 15000 filename.log | cut -f 4 -d ' ' | cut -f 1,2 -d ':' | sort | uniq -c | more

The main difference here is that I opted for the 4th (rather than 1st) result of the cut command, which gets me the timestamp element (rather than IP address), and then a second cut command parses it on the colon symbol and returns the first (date) and second (hour) for further grouping.

Finally, I tweaked it a little bit more so I get an idea of whether there was excessive requests within any minute-span. This can be achieved by expanding the second cut command slightly, as per below.

tail -n 15000 filename.log | cut -f 4 -d ' ' | cut -f 1,2,3 -d ':' | sort | uniq -c | more

Querying Dates and Date Parts in Laravel

The World War II timeline application allows users to query historical events by a specific date, by month/day combination, by a year, or they can leave everything blank and the system will show them events that had happened “on this day”. Here’s an example for 18 March 1944. To achieve this, in SQL speak, it would be something like the following.

-- By specific date
select * from my_table where my_date=:yyyymmdd;

-- By year
select * from my_table where date_format(my_date, '%Y')=:yyyy;

In Laravel’s This can be achieved with several Laravel Eloquent’s built in functions: whereDate(), whereYear(), whereMonth(), and whereDay(). Below are some examples from WW2DB’s TimelineController.

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Timeline;

class TimelineController extends Controller {

	/* ... Unrelated code omitted from this article */

	public function getList(Request $request) {
		/* ... Unrelated code omitted from this article */

		$mm = $request->query('mm');
		$dd = $request->query('dd');
		$yyyy = $request->query('yyyy');
		
		$timeline = Timeline::orderBy('my_date');
		
		// If querying by specific date, use whereDate()
		$timeline = $timeline->whereDate('my_date', date($yyyy."-".$mm."-".$dd));
		
		// If querying by month/day, use whereMonth() + whereDay()
		$timeline = $timeline->whereMonth('my_date', $mm)
			->whereDay('my_date', $dd);

		// If querying by year, use whereYear()
		$timeline = $timeline->whereYear('my_date', $yyyy);
		
		$timeline = $timeline->get();
		
		return view('timeline.my_view', compact('timeline'));
	}

	/* ... Unrelated code omitted from this article */
}

Or, simplified whereMonth() + whereDay() example to get data for 18 March:

$mm = "03";
$dd = "18";
$timeline = Timeline::orderBy('my_date');		
$timeline = $timeline->whereMonth('my_date', $mm)
	->whereDay('my_date', $dd);
$timeline = $timeline->get();

Submitting Multiple Products and Variable Quantities to PayPal

I recently did some work for a small local non-profit group to accept payment over PayPal. They have multiple, but limited, offerings that they were selling to raise funds, and customers could purchase in variable quantities. They originally used the buttons auto-generated by PayPal, but that functionality forces their customers to go back and forth between the single-page sales page and the PayPal shopping cart, which creates too much complexity for their simple sales process. Below is a simplified version of what I put together for them.

First, this is the sales form:


	<form method="post">
		<table border="0">
			<tr><td>Name</td><td>&nbsp;</td><td><input type="text" name="name" id="name" maxlength="100" /></td></tr>
			<tr><td>Email</td><td>&nbsp;</td><td><input type="text" name="email" id="email" maxlength="100" /></td></tr>
			<tr><td>Phone</td><td>&nbsp;</td><td><input type="text" name="phone" id="phone" maxlength="100" /></td></tr>
			<tr><td colspan="9"><br />I wish to purchase:<br /><br /></td></tr>
			<tr><td>Product 1</td><td>&nbsp;</td><td><select name="prod1"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $5 ea.</td></tr>
			<tr><td>Product 2</td><td>&nbsp;</td><td><select name="prod2"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $5 ea.</td></tr>
			<tr><td>Product 3</td><td>&nbsp;</td><td><select name="prod3"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $10 ea.</td></tr>
			<tr><td>Product 4</td><td>&nbsp;</td><td><select name="prod4"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $20 ea.</td></tr>
		</table>
		<input type="submit" value="Review Order" />
	</form>

The next page grabs the values passed in from the previous page and displays a confirmation screen for the customer to review; if all looks ok, the customer will submit the following form. Note the specific form action and hidden inputs (cmd, upload, business, and currency_code) required by PayPal. Multiple products is achieved by creating sets of inputs (item_name_*, quantity_*, amount_*) for each product, with the asterisk being a number; the numbering must start from 1 and subsequent products must be in order, ie. 2, 3, 4, etc., thus the use of the IF statements and the $itemCnt counter variable. The amount_* field represents the unit price of the associated product, not the line total.


	<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
		<input type="hidden" name="cmd" value="_cart">
		<input type="hidden" name="upload" value="1">
		<input type="hidden" name="business" value="store@email.here">
		<input type="hidden" name="currency_code" value="USD">
		<?php
		$itemCnt = 1;
		if ($prod1 != "0") {
			?>
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 1">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod1); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="5">
			<?php
			$itemCnt++;
		}
		if ($prod2 != "0") {
			?>
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 2">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod2); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="5">
			<?php
			$itemCnt++;
		}
		if ($prod3 != "0") {
			?>
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 3">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod3); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="10">
			<?php
			$itemCnt++;
		}
		if ($prod4 != "0") {
			?>
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 4">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod4); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="20">
			<?php
			$itemCnt++;
		}
		// Repeat as necessary...
		?>
	</form>

Connecting PHP to Oracle with OCI8

Below is the set of steps I took to connect a XAMPP setup to an Oracle database server. I am doing this on a server running Windows Server 2016.

  1. Download and install XAMPP. I chose the one bundled with the 32-bit version of PHP 7.3.2 with thread safety. Sourceforge: xampp-win32-7.3.2-0-VC15-installer.exe
  2. Download and install Oracle Instant Client. Because the PHP architecture is 32-bit, Oracle Instant Client must also be 32-bit. Take note of your Oracle server’s version, and install the appropriate client. Oracle: Oracle Instant Client Downloads
  3. Set up your sqlnet.ora and tnsnames.ora files at %ORACLE_HOME%\network\admin\. These two files together will tell your Oracle Instant Client how to get to your Oracle server. Sample sqlnet.ora:
    NAMES.DEFAULT_DOMAIN = MYDOMAIN.NET
    SQLNET.AUTHENTICATION_SERVICES = (NTS)
    NAMES.DIRECTORY_PATH = (TNSNAMES, ONAMES, HOSTNAME)
    

    Sample tnsnames.ora:

    DB.MYDOMAIN.NET =
    	(DESCRIPTION =
    		(ADDRESS_LIST =
    			(ADDRESS = (PROTOCOL = TCP)(HOST = odbserver3.mydomain.net)(PORT = 1521))
    		)
    		(CONNECT_DATA =
    			(SERVICE_NAME = db)
    		)
    	)
    
  4. In Windows, add the folder of Oracle Instant Client to the PATH system variable. Create a new system variable called ORACLE_HOME and put the same path in there as well.
  5. Download the OCI8 package. I chose the one labeled “7.3 Thread Safe (TS) x86” in order to match my PHP version, thread safe configuration, and the 32-bit architecture. Once downloaded, extract either php_oci8.dll, php_oci8_11g.dll, or php_oci8_12c.dll (depending on your Oracle server version) and place the DLL file in the PHP Extensions directory. By default, that folder for my installation of XAMPP is C:\xampp\php\ext\. PHP PECL: OCI8 for Windows
  6. Launch XAMPP Control Panel and start the Apache server. Add phpinfo(); to one of your pages to see if OCI8 has its own section; if so, you are ready to make your connection to your Oracle server. Here’s a quick and dirty PHP snippet you can use to do a quick connection test:
    $conn = oci_connect($user, $password, $server);
    $sql = "select user from dual";
    $stid = oci_parse($conn, $sql);
    oci_execute($stid);
    $row = oci_fetch_array($stid, OCI_BOTH);
    print($row["USER"]);
    oci_free_statement($stid);
    

Recurse through a directory with PHP

The PHP code below goes through a directory/folder recursively and display the files found as HTML links on a web page.

// Configuration
$cDrivePath = "C:\\inetpub\\site3\\filelibrary\\"; // Be sure to include slash at the end!
$webPath = "/filelibrary/"; // Be sure to include slash at the end!
// End configuration

function cleanUpFileName($fName, $removeThisString) {
	$ret = $fName;
	$ret = str_replace($removeThisString, "", $ret);
	$ret = str_replace("\\", " > ", $ret);
	return $ret;
}

function printDirContents(
		$path, 			// This is the HDD path to recurse into
		$cDrivePath, 	// Always carry the $cDrivePath value; used for beautifying text
		$webPath		// Always carry the $webPath value; used for beautifying text
	) {

	// Make sure we have a trailing slash and asterix
	$path = rtrim($path, "\\") . "\\*";

	// Make an array to hold all the sub-directories
	$dirs = glob($path, GLOB_ONLYDIR);
	
	// Make an array to hold all the files
	$files = glob($path); //This contains both sub-directories and files
	$files = array_diff($files, $dirs); // Remove sub-directories from the array
	
	// Print files
	foreach ($files as &$f) {
		$fnDownload = str_replace($cDrivePath, "", $f);
		$fnDisplay = cleanUpFileName($f, str_replace("*","",$path));
		print("[a href='".$webPath.$fnDownload."']".$fnDisplay."[/a]");
	}
	
	// Recurse into sub-directories
	foreach ($dirs as &$d) {
		print("[h3]".(cleanUpFileName($d, $cDrivePath))."[/h3]";
		// Print folder
		printDirContents($d, $cDrivePath, $webPath);
	}
}
$displayArray = printDirContents($cDrivePath, $cDrivePath, $webPath);

PowerShell script for exporting Print Server info to Oracle

The example below covers how a PowerShell script can connect to a Windows Print Server, how it can query printers information, and how it can connect to an Oracle database to perform a query.

# Start configuring parameters
Param (
	[string]$Printservers = "printserver1",
	[string]$OracleServer = "orcl",
	[string]$OracleUser = "scott",
	[string]$OraclePassword = "tiger"
	[string]$sql = "insert into printer_list (print_server, printer_name, printer_location, printer_comment, printer_ip, printer_driver_name, printer_driver_version, printer_driver, entry_dt) values(:print_server, :printer_name, :printer_location, :printer_comment, :printer_ip, :printer_driver_name, :printer_driver_version, :printer_driver, sysdate) "
)
# End configuring parameters

ForEach ($Printserver in $Printservers) { # Start looping through each print server
	$Printers = Get-WmiObject Win32_Printer -ComputerName $Printserver

	ForEach ($Printer in $Printers) { # Start looping through each printer
		[System.Reflection.Assembly]::LoadWithPartialName("System.Data.OracleClient")
		$connectionString = "User Id=$OracleUser;Password=$OraclePassword;Data Source=$OracleServer;"
		$connection = $null
		$command = $null

		Try {
			$connection = New-Object System.Data.OracleClient.OracleConnection($connectionString)
			$command = New-Object System.Data.OracleClient.OracleCommand -ArgumentList $sql, $connection
			$connection.Open()
			
			$NoOutput = $command.Parameters.Add("print_server", $Printserver)
			$NoOutput = $command.Parameters.Add("printer_name", $Printer.Name)
			
			$Location = $Printer.Location
			if (!$Location) {
				$Location = " "
			}
			$NoOutput = $command.Parameters.Add("printer_location", $Location)
			
			$Comment = $Printer.Comment
			if (!$Comment) {
				$Comment = " "
			}
			$NoOutput = $command.Parameters.Add("printer_comment", $Comment)
			
			$NoOutput = $command.Parameters.Add("printer_ip", $Printer.Portname)
			$Drivers = Get-WmiObject Win32_PrinterDriver -Filter "__path like '%$($Printer.DriverName)%'" -ComputerName $Printserver
			
			$DriverVersion = " "
			$Driver = " "
			ForEach ($Driver in $Drivers) {
				$Drive = $Driver.DriverPath.Substring(0,1)
				$DriverVersion = (Get-ItemProperty ($Driver.DriverPath.Replace("$Drive`:","\\$PrintServer\$Drive`$"))).VersionInfo.ProductVersion
				$Driver = Split-Path $Driver.DriverPath -Leaf
			}
			
			$Drivername = $Printer.Drivername
			if (!$Drivername) {
				$Drivername = " "
			}
			$NoOutput = $command.Parameters.Add("printer_driver_name", $Drivername)
			
			$NoOutput = $command.Parameters.Add("printer_driver_version", $DriverVersion)
			$NoOutput = $command.Parameters.Add("printer_driver", $Driver)
			
			$command.ExecuteNonQuery()
		}
		Finally {
			if ($connection -ne $null) {
				$connection.Close()
				$connection.Dispose()
			}

			if ($command -ne $null) {
				$command.Dispose()
			}
		}
	} # End looping through each printer
} # End looping through each print server