Initial Commit

This commit is contained in:
Amit Kumar Nandi 2024-03-02 18:58:02 +05:30
commit eb50f184b3
40 changed files with 4469 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
# Include your project-specific ignores in this file
# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
# Useful .gitignore templates: https://github.com/github/gitignore
node_modules
dist
.cache
/logs/
/.idea/
/vendor/

94
Dockerfile Normal file
View file

@ -0,0 +1,94 @@
FROM php:8.3.1-fpm
ARG WORKDIR=/var/www/html
ENV DOCUMENT_ROOT=${WORKDIR}
ENV DOMAIN=_
ENV CLIENT_MAX_BODY_SIZE=15M
ARG GROUP_ID=1000
ARG USER_ID=1000
ENV USER_NAME=www-data
ARG GROUP_NAME=www-data
# Install system dependencies
RUN apt-get update && apt-get install -y \
libmemcached-dev \
libonig-dev \
supervisor \
libzip-dev \
libpq-dev \
zip \
unzip \
cron
# Install nginx
RUN apt-get install -y nginx
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install PHP extensions zip, mbstring, exif, bcmath, intl
RUN docker-php-ext-install zip mbstring pcntl opcache bcmath -j$(nproc)
# Install the php memcached extension
RUN pecl install memcached && docker-php-ext-enable memcached
# Download Composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
php -r "unlink('composer-setup.php');"
# Create a log directory and give permissions
RUN mkdir -p /var/www/html/logs && \
chown -R www-data:www-data /var/www/html/logs
# Set working directory
WORKDIR $WORKDIR
ADD . $WORKDIR/
# Install PHP dependencies using Composer
RUN composer install
ADD docker/php.ini $PHP_INI_DIR/conf.d/
ADD docker/opcache.ini $PHP_INI_DIR/conf.d/
ADD docker/supervisord.conf /etc/supervisor/supervisord.conf
COPY docker/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
RUN ln -s /usr/local/bin/entrypoint.sh /
RUN rm -rf /etc/nginx/conf.d/default.conf
RUN rm -rf /etc/nginx/sites-enabled/default
RUN rm -rf /etc/nginx/sites-available/default
RUN rm -rf /etc/nginx/nginx.conf
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/default.conf /etc/nginx/conf.d/
RUN usermod -u ${USER_ID} ${USER_NAME}
RUN groupmod -g ${USER_ID} ${GROUP_NAME}
RUN mkdir -p /var/log/supervisor
RUN mkdir -p /var/log/nginx
RUN mkdir -p /var/cache/nginx
RUN chown -R ${USER_NAME}:${GROUP_NAME} /var/www && \
chown -R ${USER_NAME}:${GROUP_NAME} /var/log/ && \
chown -R ${USER_NAME}:${GROUP_NAME} /etc/supervisor/conf.d/ && \
chown -R ${USER_NAME}:${GROUP_NAME} $PHP_INI_DIR/conf.d/ && \
touch /var/run/nginx.pid && \
chown -R $USER_NAME:$USER_NAME /var/cache/nginx && \
chown -R $USER_NAME:$USER_NAME /var/lib/nginx/ && \
chown -R $USER_NAME:$USER_NAME /var/run/nginx.pid && \
chown -R $USER_NAME:$USER_NAME /var/log/supervisor && \
chown -R $USER_NAME:$USER_NAME /etc/nginx/nginx.conf && \
chown -R $USER_NAME:$USER_NAME /etc/nginx/conf.d/ && \
chown -R ${USER_NAME}:${GROUP_NAME} /tmp
#USER ${USER_NAME}
EXPOSE 80
ENTRYPOINT ["entrypoint.sh"]

97
README.md Normal file
View file

@ -0,0 +1,97 @@
## PulseBridge Gateway Server
Welcome to the PulseBridge Gateway Server repository! This server acts as an SMS Gateway powered by the PulseBridge library.The PulseBridge Gateway Server is a powerful SMS Gateway software that allows you to send SMS messages seamlessly. Whether you're looking to integrate SMS functionality into your web applications or send messages from a centralized server, PulseBridge Gateway makes the process efficient and straightforward.
## Table of Contents
* [Getting Started](#getting-started)
* [Building and Running Locally](#building-and-running-locally)
* [Running with Docker Compose](#running-with-docker-compose)
* [Manually Spinning Up Your Own Image](#manually-spinning-up-your-own-image)
* [Licecnse](#license)
* [Contribution](#contribution)
## Getting Started
Follow these three simple steps to get started with PulseBridge Gateway:
1. **Run PulseBridge Gateway Server App and click `setup credentials` button.
Clone the repository, install dependencies, and run the server locally or using Docker Compose using the below instructions.[Building and Running Locally](#building-and-running-locally)
2. [**Download**](https://app.download#) **PulseBridge Mobile App and set the URL in app provided by the server.**
3. **Send SMS from the Server Frontend or API**
With the PulseBridge Gateway Server running, access the user-friendly interface at [http://localhost](http://localhost/) to send SMS messages. Alternatively, integrate the SMS functionality into your applications using the provided API.
## Building and Running Locally
1. **Clone the Repository:**
```plaintext
git clone https://github.com/aamitn/pulsebridge-gateway.git
cd pulsebridge-gateway
```
2. **Install Dependencies:**
`*install composer from instructions here :` [`Composer (getcomposer.org)`](https://getcomposer.org/download/)
```plaintext
composer install
```
3. **Run the Server: \[run the dev server or copy the directory contents to web server of your choice\]**
```plaintext
php -S localhost:8000 -t public
```
## Running with Docker Compose
* run command from the project root directory
1. **Run the Container:**
```plaintext
docker-compose up -d
```
## Build your own docker image
1. **Build the Docker Image::**
```plaintext
docker build -t pulsebridge-gateway .
```
2. **Run the Docker Container:**
```plaintext
docker run -p 80:80 --name pulsebridge-gateway pulsebridge-gateway
```
## **License**
This project is licensed under the [MIT License](https://chat.openai.com/c/LICENSE).
 
## **Contributions**
Contributions are welcome! If you'd like to contribute to PulseBridge Gateway, please follow our [Contribution Guidelines](https://chat.openai.com/c/CONTRIBUTING.md).
Fork the repository and create your branch:
1. bashCopy code
`git clone https://github.com/aamitn/pulsebridge-gateway.git cd pulsebridge-gateway git checkout -b feature/your-feature`
2. Make your changes and commit them:
bashCopy code
`git add . git commit -m "Add your feature"`
3. Push to your fork and submit a pull request.
4. Follow the code review process.
5. Your contribution will be merged once approved.

21
composer.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "nmpl/pulsebridge",
"description": "description",
"minimum-stability": "stable",
"license": "proprietary",
"authors": [
{
"name": "Amit Nandi",
"email": "amit@bitmutex.com"
}
],
"autoload": {
"psr-4": {"nmpl\\pulsebridge\\": "src/"}
},
"require-dev": {
"phpunit/phpunit": "11.0.3"
},
"scripts": {
"test": "phpunit"
}
}

1643
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
{"id": "65e3290fd423b9-55994365", "to": "0123456789", "content": "Hello world"}

View file

@ -0,0 +1 @@
{"id": "65e3291be00f82-11680709", "to": "0123456789", "content": "Hello world"}

View file

@ -0,0 +1 @@
{"id": "65e329235a0cc8-06863164", "to": "0123456789", "content": "Hello world"}

View file

@ -0,0 +1 @@
{"id": "65e32925058621-25075091", "to": "0123456789", "content": "Hello world"}

16
docker-compose.yml Normal file
View file

@ -0,0 +1,16 @@
version: '3.8'
services:
pulsebridge-gateway-app:
image: nmpl/pulsebridge:latest
# build:
# context: .
container_name: pulsebridge-gateway
networks:
- pulsebridge-network
ports:
- "80:80"
networks:
pulsebridge-network:
driver: bridge

41
docker/default.conf Normal file
View file

@ -0,0 +1,41 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# Add index.php to setup Nginx, PHP & PHP-FPM config
index index.php index.html index.htm index.nginx-debian.html; error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html;
# pass PHP scripts on Nginx to FastCGI (PHP-FPM) server
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Nginx php-fpm config:
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
client_max_body_size 15M;
server_tokens off;
# Hide PHP headers
fastcgi_hide_header X-Powered-By;
fastcgi_hide_header X-CF-Powered-By;
fastcgi_hide_header X-Runtime;
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
# deny access to Apache .htaccess on Nginx with PHP,
# if Apache and Nginx document roots concur
location ~ /\.ht {deny all;}
location ~ /\.svn/ {deny all;}
location ~ /\.git/ {deny all;}
location ~ /\.hg/ {deny all;}
location ~ /\.bzr/ {deny all;}
}

98
docker/entrypoint.sh Normal file
View file

@ -0,0 +1,98 @@
#!/bin/sh
echo ""
echo "***********************************************************"
echo " Starting NGINX PHP-FPM Docker Container "
echo "***********************************************************"
set -e
set -e
info() {
{ set +x; } 2> /dev/null
echo '[INFO] ' "$@"
}
warning() {
{ set +x; } 2> /dev/null
echo '[WARNING] ' "$@"
}
fatal() {
{ set +x; } 2> /dev/null
echo '[ERROR] ' "$@" >&2
exit 1
}
# Enable custom nginx config files if they exist
if [ -f /var/www/html/conf/nginx/nginx.conf ]; then
cp /var/www/html/conf/nginx/nginx.conf /etc/nginx/nginx.conf
info "Using custom nginx.conf"
fi
if [ -f /var/www/html/conf/nginx/nginx-site.conf ]; then
info "Custom nginx site config found"
rm /etc/nginx/conf.d/default.conf
cp /var/www/html/conf/nginx/nginx-site.conf /etc/nginx/conf.d/default.conf
info "Start nginx with custom server config..."
else
info "nginx-site.conf not found"
info "If you want to use custom configs, create config file in /var/www/html/conf/nginx/nginx-site.conf"
info "Start nginx with default config..."
rm -f /etc/nginx/conf.d/default.conf
TASK=/etc/nginx/conf.d/default.conf
touch $TASK
cat > "$TASK" <<EOF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name $DOMAIN;
# Add index.php to setup Nginx, PHP & PHP-FPM config
index index.php index.html index.htm index.nginx-debian.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root $DOCUMENT_ROOT;
# pass PHP scripts on Nginx to FastCGI (PHP-FPM) server
location ~ \.php$ {
try_files \$uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Nginx php-fpm config:
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param PATH_INFO \$fastcgi_path_info;
}
client_max_body_size $CLIENT_MAX_BODY_SIZE;
server_tokens off;
# Hide PHP headers
fastcgi_hide_header X-Powered-By;
fastcgi_hide_header X-CF-Powered-By;
fastcgi_hide_header X-Runtime;
location / {
try_files \$uri \$uri/ /index.php\$is_args\$args;
gzip_static on;
}
location ~ \.css {
add_header Content-Type text/css;
}
location ~ \.js {
add_header Content-Type application/x-javascript;
}
# deny access to Apache .htaccess on Nginx with PHP,
# if Apache and Nginx document roots concur
location ~ /\.ht {deny all;}
location ~ /\.svn/ {deny all;}
location ~ /\.git/ {deny all;}
location ~ /\.hg/ {deny all;}
location ~ /\.bzr/ {deny all;}
}
EOF
fi
## Check if the supervisor config file exists
if [ -f /var/www/html/conf/worker/supervisor.conf ]; then
info "Custom supervisor config found"
cp /var/www/html/conf/worker/supervisor.conf /etc/supervisor/conf.d/supervisor.conf
fi
## Start Supervisord
supervisord -c /etc/supervisor/supervisord.conf

73
docker/nginx.conf Normal file
View file

@ -0,0 +1,73 @@
user www-data;
worker_processes auto;
error_log /var/log/nginx/error.log crit;
pid /var/run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
client_header_timeout 3m;
client_body_timeout 3m;
client_max_body_size 256m;
client_header_buffer_size 4k;
client_body_buffer_size 256k;
large_client_header_buffers 4 32k;
send_timeout 3m;
keepalive_timeout 60 60;
reset_timedout_connection on;
server_names_hash_max_size 1024;
server_names_hash_bucket_size 1024;
ignore_invalid_headers on;
connection_pool_size 256;
request_pool_size 4k;
output_buffers 4 32k;
postpone_output 1460;
include mime.types;
default_type application/octet-stream;
# Compression gzip
gzip on;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
gzip_proxied any;
gzip_min_length 512;
gzip_comp_level 6;
gzip_buffers 8 64k;
gzip_types text/plain text/xml text/css text/js application/x-javascript application/xml image/png image/x-icon image/gif image/jpeg image/svg+xml application/xml+rss text/javascript application/atom+xml application/javascript application/json application/x-font-ttf font/opentype;
# Proxy settings
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
proxy_pass_header Set-Cookie;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
proxy_buffers 32 4k;
proxy_cache_path /var/cache/nginx levels=2 keys_zone=cache:10m inactive=60m max_size=512m;
proxy_cache_key "$host$request_uri $cookie_user";
proxy_temp_path /var/cache/nginx/temp;
proxy_ignore_headers Expires Cache-Control;
proxy_cache_use_stale error timeout invalid_header http_502;
proxy_cache_valid any 1d;
server_tokens off;
include /etc/nginx/conf.d/*.conf;
}

135
docker/opcache.ini Normal file
View file

@ -0,0 +1,135 @@
[opcache]
; Determines if Zend OPCache is enabled
opcache.enable=1
; Determines if Zend OPCache is enabled for the CLI version of PHP
;opcache.enable_cli=1
; The OPcache shared memory storage size.
opcache.memory_consumption=512
; The amount of memory for interned strings in Mbytes.
opcache.interned_strings_buffer=64
; The maximum number of keys (scripts) in the OPcache hash table.
; Only numbers between 200 and 1000000 are allowed.
;If you have multiple PHP sites on the server then consider the value 130986
; for magento 2, keep 65406
opcache.max_accelerated_files=50000
; The maximum percentage of "wasted" memory until a restart is scheduled.
opcache.max_wasted_percentage=15
; When this directive is enabled, the OPcache appends the current working
; directory to the script key, thus eliminating possible collisions between
; files with the same name (basename). Disabling the directive improves
; performance, but may break existing applications.
;opcache.use_cwd=1
; When disabled, you must reset the OPcache manually or restart the
; webserver for changes to the filesystem to take effect.
; For Development / testing, keep 1
; For performance / production, keep 0
opcache.validate_timestamps=0
;opcache.revalidate_freq How often in seconds should the code
;cache expire and check if your code has changed. 0 means it
;checks your PHP code every single request IF YOU HAVE
;opcache.validate_timestamps ENABLED. opcache.validate_timestamps
;should not be enabled by default, as long as it's disabled then any value for opcache.
;revalidate_freq will basically be ignored. You should really only ever enable
;this during development, you don't really want to enable this setting for a production application.
opcache.revalidate_freq=0
; Enables or disables file search in include_path optimization
;opcache.revalidate_path=0
; If disabled, all PHPDoc comments are dropped from the code to reduce the
; size of the optimized code.
opcache.save_comments=1
; If enabled, a fast shutdown sequence is used for the accelerated code
; Depending on the used Memory Manager this may cause some incompatibilities.
opcache.fast_shutdown=1
; Allow file existence override (file_exists, etc.) performance feature.
;opcache.enable_file_override=0
; A bitmask, where each bit enables or disables the appropriate OPcache
; passes
;opcache.optimization_level=0xffffffff
;opcache.inherited_hack=1
;opcache.dups_fix=0
; The location of the OPcache blacklist file (wildcards allowed).
; Each OPcache blacklist file is a text file that holds the names of files
; that should not be accelerated. The file format is to add each filename
; to a new line. The filename may be a full path or just a file prefix
; (i.e., /var/www/x blacklists all the files and directories in /var/www
; that start with 'x'). Line starting with a ; are ignored (comments).
;opcache.blacklist_filename=
; Allows exclusion of large files from being cached. By default all files
; are cached.
;opcache.max_file_size=0
; Check the cache checksum each N requests.
; The default value of "0" means that the checks are disabled.
;opcache.consistency_checks=0
; How long to wait (in seconds) for a scheduled restart to begin if the cache
; is not being accessed.
;opcache.force_restart_timeout=180
; OPcache error_log file name. Empty string assumes "stderr".
;opcache.error_log=
; All OPcache errors go to the Web server log.
; By default, only fatal errors (level 0) or errors (level 1) are logged.
; You can also enable warnings (level 2), info messages (level 3) or
; debug messages (level 4).
;opcache.log_verbosity_level=1
; Preferred Shared Memory back-end. Leave empty and let the system decide.
;opcache.preferred_memory_model=
; Protect the shared memory from unexpected writing during script execution.
; Useful for internal debugging only.
;opcache.protect_memory=0
; Allows calling OPcache API functions only from PHP scripts which path is
; started from specified string. The default "" means no restriction
;opcache.restrict_api=
; Mapping base of shared memory segments (for Windows only). All the PHP
; processes have to map shared memory into the same address space. This
; directive allows to manually fix the "Unable to reattach to base address"
; errors.
opcache.mmap_base=0x20000000
; Enables and sets the second level cache directory.
; It should improve performance when SHM memory is full, at server restart or
; SHM reset. The default "" disables file based caching.
;opcache.file_cache=
; Enables or disables opcode caching in shared memory.
;opcache.file_cache_only=0
; Enables or disables checksum validation when script loaded from file cache.
;opcache.file_cache_consistency_checks=1
; Implies opcache.file_cache_only=1 for a certain process that failed to
; reattach to the shared memory (for Windows only). Explicitly enabled file
; cache is required.
opcache.file_cache_fallback=1
; Enables or disables copying of PHP code (text segment) into HUGE PAGES.
; This should improve performance, but requires appropriate OS configuration.
;opcache.huge_code_pages=1
; Validate cached file permissions.
; opcache.validate_permission=0
; Prevent name collisions in chroot'ed environment.
; opcache.validate_root=0

6
docker/php.ini Normal file
View file

@ -0,0 +1,6 @@
date.timezone=UTC
display_errors=Off
log_errors=On
upload_max_filesize= 80M
post_max_size= 80M
memory_limit = 256M

32
docker/supervisord.conf Normal file
View file

@ -0,0 +1,32 @@
[supervisord]
nodaemon=true
user=www-data
logfile=/var/log/supervisor/supervisord.log
logfile_maxbytes = 50MB
pidfile=/tmp/supervisord.pid
directory = /tmp
[program:php-fpm]
command=/usr/local/sbin/php-fpm
numprocs=1
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/php-fpm.err.log
stdout_logfile=/var/log/supervisor/php-fpm.out.log
user=www-data
priority=1
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
numprocs=1
autostart=true
autorestart=true
stderr_logfile=/var/log/nginx/nginx.err.log
stdout_logfile=/var/log/nginx/nginx.out.log
logfile_maxbytes = 50MB
user=www-data
priority=2
[include]
files = /etc/supervisor/conf.d/*.conf

28
error.php Normal file
View file

@ -0,0 +1,28 @@
<?php
// error.php
// Check if the error message is provided in the URL
if (isset($_GET["message"])) {
$errorMessage = $_GET["message"];
// Set the HTTP status code
http_response_code(400); // You can change this to the appropriate HTTP status code
// Define the JSON response
$response = [
'error' => true,
'message' => $errorMessage,
];
// Set the content type to JSON
header('Content-Type: application/json');
// Output the JSON response
echo json_encode($response);
exit();
} else {
// If no error message is provided, redirect to a generic error page or home page
header("Location: /"); // You can change this to the appropriate URL
exit();
}
?>

37
index.html Normal file
View file

@ -0,0 +1,37 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Redirecting to index.php</title>
<link rel="stylesheet" href="resources/css/style.css">
<meta name="description" content="">
<meta property="og:title" content="">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
<link rel="icon" href="resources/favicon.ico" sizes="any">
<link rel="icon" href="resources/logo.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="logo.svg">
<link rel="manifest" href="site.webmanifest">
<meta name="theme-color" content="#fafafa">
<script>
</script>
</head>
<body>
<!-- Add your site or application content here -->
<p>Greetings from PulseBridge</p>
<div id="error-container"></div>
<script src="resources/js/app.js"></script>
</body>
</html>

155
index.php Normal file
View file

@ -0,0 +1,155 @@
<?php
namespace nmpl\pulsebridge;
class App
{
public function dependencyInstaller(): void
{
// Check if the autoload.php file exists
$autoloadPath = __DIR__ . '/vendor/autoload.php';
if (!file_exists($autoloadPath)) {
echo '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="PulseBridge - SMS Gateway Software">
<meta property="og:title" content="PulseBridge - SMS Gateway Software">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
<link rel="icon" href="./resources/favicon.ico" sizes="any">
<link rel="icon" href="./resources/logo.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="./resources/logo.svg">
<link rel="manifest" href="site.webmanifest">
<meta name="theme-color" content="#fafafa">
<title>Installation-PulseBridge</title>
<link rel="icon" href="./resources/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="./resources/css/fontawesome.min.css" crossorigin="anonymous">
</head>
<body>
<img src="./resources/logo.svg" alt="logo" style="display: block; margin: 10px auto; width: 120px; height: auto; border-radius: 10px; box-shadow: 0 4px 8px rgb(37, 150, 190); "/>
';
// Output the result of the shell command
echo '<div style="max-width: 600px; margin: auto; padding: 20px; border: 2px solid #3498db; background-color: #ecf0f1; border-radius: 10px; text-align: center;">';
echo "<h2 style='color: #3498db;'>Application Installation</h2>";
echo "<p style='font-size: 18px; margin-bottom: 20px;'>Before installing, make sure you have Composer installed on your system. If not, follow the steps below:</p>";
echo "<p style='font-size: small; color: #2c3e50;'>**Make sure you have shell_exec() enabled in your php.ini</p>";
// Step1: Download Composer
echo "<h3 style='color: #2c3e50;'>Step 1: Download Composer</h3>";
echo "<p style='font-size: 16px; color: #2c3e50;'>Visit <a href='https://getcomposer.org/download/' target='_blank' style='color: #3498db; text-decoration: none;'>Composer Download Page</a> to download and install Composer on your system or click the button below:</p>";
// Check if composer.phar is not present in the project root
if (!file_exists('composer.phar')) {
// Display the form to download and install Composer
echo '<form id="composerForm" method="post">';
echo '<input type="submit" name="composerCommand" value="Download & Install Composer" style="background-color: #3498db; color: #fff; padding: 10px 15px; border: none; cursor: pointer; font-size: 16px; border-radius: 5px;">';
echo '</form>';
} else {
// Display a success message with a completed icon
echo '<div style="text-align: center; color: #2ecc71; font-size: 18px;">';
echo '<i class="fa-solid fa-circle-check" style="color: #2ecc71; font-size: 24px;"></i> Composer is installed successfully!';
echo '</div>';
}
echo "<hr style='border-color: #3498db; margin: 20px 0;'>";
// Step2: Install App
echo "<h3 style='color: #2c3e50;'>Step 2: Install Application</h3>";
echo "<p style='font-size: 16px; color: #2c3e50;'>Once Composer is installed, run <strong>composer install</strong> in your terminal or click the button below:</p>";
// Output a professional-looking HTML button that triggers another shell command when clicked
echo '<form id="executeForm" method="post">';
echo '<input type="submit" name="executeCommand" value="Install Application" style="background-color: #3498db; color: #fff; padding: 10px 15px; border: none; cursor: pointer; font-size: 16px; border-radius: 5px;">';
echo '</form>';
// Output PHP version and server info
echo "<p style='font-size: 16px; margin-top: 20px; color: #2c3e50;'>Server Information: " . $_SERVER['SERVER_SOFTWARE'] . "</p>";
// Check if the button is clicked
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['executeCommand'])) {
// Replace 'your-other-shell-command' with the actual shell command you want to execute
$otherShellCommand = 'php composer.phar install';
// Execute the other shell command
$otherOutput = shell_exec($otherShellCommand);
// Output the result of the other shell command
echo "<p style='font-size: 16px; margin-top: 20px; color: #2c3e50;'>Application Successfully Installed! Redirecting in 5 Seconds <pre style='font-size: 14px; background-color: #ecf0f1; padding: 10px; border-radius: 5px; overflow-x: auto;'>$otherOutput</pre></p>";
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['composerCommand'])) {
$command1 = "php -r \"copy('https://getcomposer.org/installer', 'composer-setup.php');\"";
$command2 = "php composer-setup.php";
$command3 = "php -r \"unlink('composer-setup.php');\"";
// Execute the other shell command
$otherOutput1 = shell_exec($command1);
$otherOutput2 = shell_exec($command2);
$otherOutput3 = shell_exec($command3);
echo "<p style='font-size: 16px; margin-top: 20px; color: #2c3e50;'>Installing...:
<pre style='font-size: 14px; background-color: #ecf0f1; padding: 10px; border-radius: 5px; overflow-x: auto;'>$otherOutput1</pre>
<pre style='font-size: 14px; background-color: #ecf0f1; padding: 10px; border-radius: 5px; overflow-x: auto;'>$otherOutput2</pre>
<pre style='font-size: 14px; background-color: #ecf0f1; padding: 10px; border-radius: 5px; overflow-x: auto;'>$otherOutput3</pre>
</p>";
header("Refresh:0");
}
// You can choose to exit here or continue with the rest of your code
echo '</div><footer style="text-align: center;">
<p>Bitmutex Technologies &copy; '. date ('Y') .' | <a href="mailto:amit@bitmutex.com">support@bitmutex.com</a></p>
</footer>';
exit();
}
// Include the autoload.php file
require $autoloadPath;
}
public function run(): void
{
$logger = new Logger(__DIR__ . '/logs/');
$driver = new Driver($logger);
}
}
// Instantiate and run the App
$app = new App();
// If the button is clicked, wait for 2 seconds using JavaScript before submitting the form
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['executeCommand'])) {
echo '<script>
setTimeout(function() {
document.getElementById("executeForm").submit();
}, 2000);
setTimeout(function() {
document.getElementById("composerForm").submit();
}, 2000);
</script>';
}
$app->dependencyInstaller();
$app->run();

25
phpunit.xml Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="true" colors="true"
processIsolation="true" stopOnFailure="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Pulsebridge Test Suite">
<directory>src\test</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
</php>
<source>
<include>
<directory suffix=".php">src/test/</directory>
</include>
</source>
</phpunit>

7
resources/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

9
resources/css/fontawesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

247
resources/css/style.css Normal file
View file

@ -0,0 +1,247 @@
/*! HTML5 Boilerplate v9.0.0-RC1 | MIT License | https://html5boilerplate.com/ */
/* main.css 3.0.0 | MIT License | https://github.com/h5bp/main.css#readme */
/*
* What follows is the result of much research on cross-browser styling.
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
* Kroc Camen, and the H5BP dev community and team.
*/
/* ==========================================================================
Base styles: opinionated defaults
========================================================================== */
html {
color: #222;
font-size: 1em;
line-height: 1.4;
}
/*
* Remove text-shadow in selection highlight:
* https://twitter.com/miketaylr/status/12228805301
*
* Customize the background color to match your design.
*/
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
/*
* A better looking default horizontal rule
*/
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #ccc;
margin: 1em 0;
padding: 0;
}
/*
* Remove the gap between audio, canvas, iframes,
* images, videos and the bottom of their containers:
* https://github.com/h5bp/html5-boilerplate/issues/440
*/
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: middle;
}
/*
* Remove default fieldset styles.
*/
fieldset {
border: 0;
margin: 0;
padding: 0;
}
/*
* Allow only vertical resizing of textareas.
*/
textarea {
resize: vertical;
}
/* ==========================================================================
Author's custom styles
========================================================================== */
/* ==========================================================================
Helper classes
========================================================================== */
/*
* Hide visually and from screen readers
*/
.hidden,
[hidden] {
display: none !important;
}
/*
* Hide only visually, but have it available for screen readers:
* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
*
* 1. For long content, line feeds are not interpreted as spaces and small width
* causes content to wrap 1 word per line:
* https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
*/
.visually-hidden {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
/* 1 */
}
/*
* Extends the .visually-hidden class to allow the element
* to be focusable when navigated to via the keyboard:
* https://www.drupal.org/node/897638
*/
.visually-hidden.focusable:active,
.visually-hidden.focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
white-space: inherit;
width: auto;
}
/*
* Hide visually and from screen readers, but maintain layout
*/
.invisible {
visibility: hidden;
}
/*
* Clearfix: contain floats
*
* The use of `table` rather than `block` is only necessary if using
* `::before` to contain the top-margins of child elements.
*/
.clearfix::before,
.clearfix::after {
content: "";
display: table;
}
.clearfix::after {
clear: both;
}
/* ==========================================================================
EXAMPLE Media Queries for Responsive Design.
These examples override the primary ('mobile first') styles.
Modify as content requires.
========================================================================== */
@media only screen and (min-width: 35em) {
/* Style adjustments for viewports that meet the condition */
}
@media print,
(-webkit-min-device-pixel-ratio: 1.25),
(min-resolution: 1.25dppx),
(min-resolution: 120dpi) {
/* Style adjustments for high resolution devices */
}
/* ==========================================================================
Print styles.
Inlined to avoid the additional HTTP request:
https://www.phpied.com/delay-loading-your-print-css/
========================================================================== */
@media print {
*,
*::before,
*::after {
background: #fff !important;
color: #000 !important;
/* Black prints faster */
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]::after {
content: " (" attr(href) ")";
}
abbr[title]::after {
content: " (" attr(title) ")";
}
/*
* Don't show links that are fragment identifiers,
* or use the `javascript:` pseudo protocol
*/
a[href^="#"]::after,
a[href^="javascript:"]::after {
content: "";
}
pre {
white-space: pre-wrap !important;
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
tr,
img {
page-break-inside: avoid;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}

BIN
resources/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

5
resources/js/app.js Normal file
View file

@ -0,0 +1,5 @@
window.onload = function() {
setTimeout(function() {
window.location.href = 'index.php';
}, 0);
}

7
resources/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
resources/js/jquery-3.6.4.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
resources/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

0
resources/js/vendor/.gitkeep vendored Normal file
View file

10
resources/logo.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="#2ecc71">
<!-- Background Circle -->
<circle cx="32" cy="32" r="30" fill="#3498db" />
<!-- SMS Icon -->
<g fill="#fff">
<rect x="25" y="22" width="14" height="4" rx="2" />
<path d="M46.6 14.4a2 2 0 00-2.8 0L32 26.2l-8.8-8.8a2 2 0 00-2.8 0 2 2 0 000 2.8l10.6 10.6a2 2 0 002.8 0L46.6 17a2 2 0 000-2.8z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 423 B

5
robots.txt Normal file
View file

@ -0,0 +1,5 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:

12
site.webmanifest Normal file
View file

@ -0,0 +1,12 @@
{
"short_name": "",
"name": "",
"icons": [{
"src": "icon.png",
"type": "image/png",
"sizes": "192x192"
}],
"start_url": "/?utm_source=homescreen",
"background_color": "#fafafa",
"theme_color": "#fafafa"
}

41
src/Driver.php Normal file
View file

@ -0,0 +1,41 @@
<?php
namespace Nmpl\Pulsebridge;
require __DIR__ . '/../vendor/autoload.php';
class Driver
{
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
// Log a message indicating the initialization
$this->logger->log('Driver initialization started.');
// Some tricks to load the Pulsebridge and PageRenderer classes in different situations
if (!class_exists('Nmpl\Pulsebridge\Pulsebridge') || !class_exists('Nmpl\Pulsebridge\PageRenderer')) {
if (file_exists('src\Pulsebridge.php') && file_exists('src\PageRenderer.php')) {
// Quick load of Pulsebridge and PageRenderer without using composer
require_once 'Pulsebridge.php';
require_once 'PageRenderer.php';
// Log a message indicating the successful loading of classes
$this->logger->log('Pulsebridge and PageRenderer classes loaded without Composer.');
} else {
// Composer autoload
require __DIR__ . '/../vendor/autoload.php';
// Log a message indicating the use of Composer autoload
$this->logger->log('Pulsebridge and PageRenderer classes loaded using Composer autoload.');
}
}
// Log a message indicating the completion of initialization
$this->logger->log('Driver initialization completed.');
}
}

24
src/Logger.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace Nmpl\Pulsebridge;
class Logger
{
private $logPath;
public function __construct($logPath)
{
$this->logPath = $logPath;
if (!file_exists($logPath)) {
mkdir($logPath, 0777, true);
}
}
public function log($message)
{
$logFile = $this->logPath . 'app_log_' . date('Y-m-d') . '.log';
ini_set("error_log", $logFile);
error_log('[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL, 3, $logFile);
}
}

583
src/PageRenderer.php Normal file
View file

@ -0,0 +1,583 @@
<?php
namespace Nmpl\Pulsebridge;
require __DIR__ . '/../vendor/autoload.php';
class PageRenderer
{
public static function renderHeader($title)
{
if (!isset($smsgateway)) {
$smsgateway = new Pulsebridge();
$smsgateway->setDataPath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR);
}
echo '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="PulseBridge - SMS Gateway Software">
<meta property="og:title" content="PulseBridge - SMS Gateway Software">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
<link rel="icon" href="./resources/favicon.ico" sizes="any">
<link rel="icon" href="./resources/logo.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="./resources/logo.svg">
<link rel="manifest" href="site.webmanifest">
<meta name="theme-color" content="#fafafa">
<title>' . $title . '</title>
<link rel="icon" href="./resources/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="./resources/css/bootstrap.min.css" >
<link rel="stylesheet" href="./resources/css/fontawesome.min.css" crossorigin="anonymous">
</head>
<body>
<script src="./resources/js/jquery-3.6.4.slim.min.js" ></script>
<script src="./resources/js/popper.min.js" ></script>
<script src="./resources/js/bootstrap.min.js"></script>
';
// Navbar
echo '<header class="bg-dark text-white py-3">
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- Include the SVG logo -->
<a class="navbar-brand" href="index.php">
<img src="resources/logo.svg" width="30" height="30" class="d-inline-block align-top" alt="Logo">
Pulsebridge
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-toggle="modal" data-target="#faqModal">FAQ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://wa.link/9exku8">Contact</a>
</li>
</ul>
</div>
</nav>
</div>
</header>';
// FAQ Modal
echo '<div class="modal fade" id="faqModal" tabindex="-1" role="dialog" aria-labelledby="faqModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="faqModalLabel">Frequently Asked Questions</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- Add your FAQ content here -->
<p><strong>Q: What is the SMS Gateway application?</strong></p>
<p><strong>A:</strong> The SMS Gateway application is a software that runs on an Android mobile phone, serving as a bridge between web interfaces and SMS functionality.</p>
<p><strong>Q: How does it work?</strong></p>
<p><strong>A:</strong> The SMS Gateway application on the mobile phone receives commands and messages from the web interface, allowing users to send and receive SMS messages programmatically.</p>
<p><strong>Q: Can I use the SMS Gateway for business purposes?</strong></p>
<p><strong>A:</strong> Yes, the SMS Gateway is designed to support business use cases, enabling you to integrate SMS functionality into your applications, services, or processes.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>';
echo '<div class="container mt-4">';
echo '<h1 class="mt-4 display-4 text-center">PulseBridge<sup style="font-size: 30px;">' . $smsgateway->getVersion() . '</sup></h1>';
echo '<p class="mt-4 display-4 text-center" style="font-size: large;">SMS gateway web-application with an HTTP interface to connect with a PulseBridge android <a href="#">app</a> and send/receive SMS. </p>';
echo '<p class="mt-4 display-4 text-center" style="font-size: small;">App Version: ' . $smsgateway->getVersion() . '</p>';
echo '<hr>';
}
public static function renderFooter()
{
echo '</div>'; // Close the container div
echo '<footer class="mt-5 text-center">
<div class="row">
<div class="col">
<strong>Server Time:</strong> <span id="serverTime"></span> -
<strong>Client Time:</strong> <span id="clientTime"></span>
</div>
</div>
<div class="row">
<div class="col">
<strong>PHP Run Mode:</strong> ' . php_sapi_name() . '
<strong>Web Server:</strong> ' . $_SERVER['SERVER_SOFTWARE'] .
(function_exists('apache_get_version') ? apache_get_version() : '') . '
</div>
</div>
<div class="row">
<div class="col">
<hr>
<small class="text-muted">Pulsebridge - Bitmutex Technologies &copy; ' . date("Y") . '</small>
</div>
</div>
</footer>';
echo '<script>
function updateServerTime() {
var currentTime = new Date();
var hours = currentTime.getHours();
var minutes = currentTime.getMinutes();
var seconds = currentTime.getSeconds();
// Add leading zero if needed
minutes = (minutes < 10 ? "0" : "") + minutes;
seconds = (seconds < 10 ? "0" : "") + seconds;
var formattedTime = hours + ":" + minutes + ":" + seconds;
document.getElementById("serverTime").innerHTML = formattedTime;
}
function updateClientTime() {
var currentTime = new Date();
var hours = currentTime.getHours();
var minutes = currentTime.getMinutes();
var seconds = currentTime.getSeconds();
// Add leading zero if needed
minutes = (minutes < 10 ? "0" : "") + minutes;
seconds = (seconds < 10 ? "0" : "") + seconds;
var formattedTime = hours + ":" + minutes + ":" + seconds;
document.getElementById("clientTime").innerHTML = formattedTime;
}
// Update server time every second
setInterval(updateServerTime, 1000);
// Update client time every second
setInterval(updateClientTime, 1000);
// Initial updates
updateServerTime();
updateClientTime();
</script>';
echo '</body></html>';
}
}
// Create an Pulsebridge instance if not done yet, and define the flat-file data folder
if (!isset($smsgateway)) {
$smsgateway = new Pulsebridge();
$smsgateway->setDataPath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR);
}
// Detect the URL with php file
//$url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . ($_SERVER['PHP_SELF']);
// Detect the URL without php file
$url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'];
// Retrieve some parameters
$command = isset($_GET["m"]) ? "m" : (isset($_GET["i"]) ? "i" : (isset($_GET["e"]) ? "e" : ""));
$h = isset($_GET["h"]) ? $_GET["h"] : "";
$mid = isset($_GET["mid"]) ? $_GET["mid"] : "";
// Correct the international format of the phone number if needed
$to = isset($_GET["to"]) ? $_GET["to"] : "";
// Validate the "to" field
if (!empty($to) && !preg_match('/^\d{10}$/', $to)) {
// Handle validation error (e.g., redirect to an error page)
header("Location: error.php?message=Invalid phone number");
exit();
}
if ("00" == substr($to, 0, 2)) {
$to = "+" . substr($to, 2, strlen($to) - 2);
}
// Define a default message if needed
$message = isset($_GET["message"]) ? $_GET["message"] : ""; // Hello World 😉
// Validate the "message" field
if (!empty($message) && strlen($message) < 5) {
// Handle validation error (e.g., redirect to an error page)
header("Location: error.php?message=Message should be at least 5 characters");
exit();
}
// Retrieve the device id
$id = isset($_GET["id"]) ? $_GET["id"] : "";
$device_id = $id;
if ((!empty($to)) && empty($device_id)) {
$device_id = substr(md5(uniqid("", true)), 0, 16);
} elseif ((empty($to)) && (!empty($device_id)) && (!file_exists($smsgateway->getDataPath() . $device_id))) {
$device_id = "";
}
// Calculate the device hash based on the secret
$device_h = $smsgateway->calculateAuthenticationHash($device_id);
// Check if device hash is valid for an existing device, otherwise flush the device id
if ((!empty($id)) && ($h != $device_h)) {
$device_id = "";
} else {
$smsgateway->updateDataStructure($id);
}
if ((!empty($mid)) && (!empty($device_id))) {
$message_state = "MISSING";
$message_array = $smsgateway->readSentStatus($id, $mid);
if (isset($message_array[0]['status'])) {
$message_state = $message_array[0]['status'];
}
echo $message_state;
} elseif ("e" == $command) {
// An enhanced command can be implemented here
} elseif (("m" == $command) && (!empty($device_id))) {
PageRenderer::renderHeader('Pulsebridge App');
//include 'header.php';
echo '<body>';
echo '<div class="container mt-4">';
// Back Button with Bootstrap styling and centered
echo '<div class="text-center"><a href="'.$_SERVER['HTTP_REFERER'].'" class="btn btn-primary mx-auto" style="border-radius: 5px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transition: background-color 0.3s;">Back</a></div>';
// Display messages resume for the "m" command
echo '<div class="container mt-5">';
echo '<div id="accordion">';
// New SMS messages received
echo '<div class="card mb-4">';
echo '<div class="card-header" id="newMessagesHeading">';
echo '<h2 class="mb-0">';
echo '<button class="btn btn-link" data-toggle="collapse" data-target="#newMessagesCollapse" aria-expanded="true" aria-controls="newMessagesCollapse">';
echo '<p style="color: #1d2124;font-size: large;">New SMS messages received <i class="fas fa-chevron-down float-right"></i></p>';
echo '</button>';
echo '</h2>';
echo '</div>';
echo '<div id="newMessagesCollapse" class="collapse show" aria-labelledby="newMessagesHeading" data-parent="#accordion">';
echo '<div class="card-body">';
$new_messages = $smsgateway->readNewMessages($id);
if (count($new_messages) > 0) {
foreach ($new_messages as $message) {
echo '<div class="card mb-2">';
echo '<div class="card-body">';
echo '<p class="text-muted"><span class="badge">' . date("Y-m-d H:i:s", $message['sms_received'] / 1000) . '</span></p>';
echo '<span class="badge badge-primary">' . $message['from'] . '</span>: ' . $message['content'];
echo '</div>';
echo '</div>';
}
} else {
echo '<p class="text-muted m-3">No messages to display.</p>';
}
echo '</div>';
echo '</div>';
echo '</div>';
// All SMS messages received
echo '<div class="card mb-4">';
echo '<div class="card-header" id="allMessagesHeading">';
echo '<h2 class="mb-0">';
echo '<button class="btn btn-link collapsed" data-toggle="collapse" data-target="#allMessagesCollapse" aria-expanded="false" aria-controls="allMessagesCollapse">';
echo '<p style="color: #1d2124;font-size: large;">All SMS messages received <i class="fas fa-chevron-down float-right"></i></p>';
echo '</button>';
echo '</h2>';
echo '</div>';
echo '<div id="allMessagesCollapse" class="collapse show" aria-labelledby="allMessagesHeading" data-parent="#accordion">';
echo '<div class="card-body">';
$all_messages = $smsgateway->readAllMessages($id);
if (count($all_messages) > 0) {
foreach ($all_messages as $message) {
echo '<div class="card mb-2">';
echo '<div class="card-body">';
echo '<p class="text-muted"><span class="badge">' . date("Y-m-d H:i:s", $message['sms_received'] / 1000) . '</span></p>';
echo '<span class="badge badge-primary">' . $message['from'] . '</span>: ' . $message['content'];
echo '</div>';
echo '</div>';
}
} else {
echo '<p class="text-muted m-3">No messages to display.</p>';
}
echo '</div>';
echo '</div>';
echo '</div>';
// All SMS messages sent
echo '<div class="card mb-4">';
echo '<div class="card-header" id="sentMessagesHeading">';
echo '<h2 class="mb-0">';
echo '<button class="btn btn-link collapsed" data-toggle="collapse" data-target="#sentMessagesCollapse" aria-expanded="false" aria-controls="sentMessagesCollapse">';
echo '<p style="color: #1d2124;font-size: large;">All SMS messages Sent <i class="fas fa-chevron-down float-right"></i></p>';
echo '</button>';
echo '</h2>';
echo '</div>';
echo '<div id="sentMessagesCollapse" class="collapse show" aria-labelledby="sentMessagesHeading" data-parent="#accordion">';
echo '<div class="card-body">';
$sent_messages = $smsgateway->readAllSentStatus($id);
if (count($sent_messages) > 0) {
foreach ($sent_messages as $message) {
echo '<div class="card mb-2">';
echo '<div class="card-body">';
echo '<p class="text-muted"><span class="badge">' . date("Y-m-d H:i:s", $message['last_update'] / 1000) . '</span></p>';
$statusBadgeClass = ($message['status'] === 'DELIVERED') ? 'badge badge-success' : 'badge badge-secondary';
echo '<span class="' . $statusBadgeClass . '">' . $message['status'] . '</span>';
echo ' <a class="badge badge-info" href="' . $url . '?id=' . $device_id . '&h=' . $h . '&mid=' . $message['message_id'] . '" target="track_' . $message['message_id'] . '">Track</a>';
echo ' : ' . $message['content'];
echo '</div>';
echo '</div>';
}
} else {
echo '<p class="text-muted m-3">No messages to display.</p>';
}
echo '</div>';
echo '</div>';
echo '</div>';
echo '</div>'; // Closing accordion container
echo '</div>';
echo '</div>';
//include 'footer.php';
PageRenderer::renderFooter();
echo '</body></html>';
}
elseif (empty($device_id) || ("i" == $command)) {
// Display basic usage info
if ("" == $to) {
$autofocus_to = "autofocus=\"autofocus\"";
$autofocus_message = "";
} else {
$autofocus_to = "";
$autofocus_message = "autofocus=\"autofocus\"";
}
//include header;
PageRenderer::renderHeader('Pulsebridge App');
echo '
<body>
<div class="container mt-4">
<form action="' . htmlspecialchars($_SERVER["PHP_SELF"]) . '" method="get" class="mb-4" onsubmit="return validateForm()" name="smsForm">';
if (!empty($id)) {
// Back Button with Bootstrap styling and centered
echo '<div class="text-center"><a href="'.$_SERVER['HTTP_REFERER'].'" class="btn btn-primary mx-auto" style="border-radius: 5px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); transition: background-color 0.3s;">Back</a></div>';
echo '<div class="form-group">';
echo ' <label for="id">Device Identification:</label>';
echo ' <input class="form-control form-control-sm font-weight-bold" size="18" type="text" name="id" placeholder="e.g. 01234567890abcdef" value="' . $id . '" readonly>';
echo '</div>';
if (!empty($h)) {
echo '<div class="form-group">';
echo ' <label for="h">Secret Hash for the Device:</label>';
echo ' <input class="form-control form-control-sm font-weight-bold" size="8" type="text" name="h" placeholder="e.g. abcdef" value="' . $h . '" readonly>';
echo '</div>';
}
}
$outputForm = ' <div class="form-group">
<label for="to">Destination mobile phone number:</label>
<input class="form-control" size="20" ' . $autofocus_to . ' type="tel" name="to" placeholder="e.g. 00123456789012" value="' . $to . '">
</div>';
$outputForm .= '
<div class="form-group">
<label for="message">Message:</label>
<textarea ' . $autofocus_message . ' class="form-control" columns="40" rows="5" placeholder="e.g. Please call me back asap !" name="message" autocomplete="on" maxlength="300" cols="80" wrap="soft">' . $message . '</textarea>
</div>
<button type="submit" class="btn btn-success">Send SMS message</button>
<button type="reset" class="btn btn-secondary">Reset</button>
<p></p>';
$outputForm .= '</form>
<script>
function goToLink() {
window.location.href = \'index.php?to=0123456789&message=Tests 🙂\';
}
function validateForm() {
var to = document.forms["smsForm"]["to"].value;
var message = document.forms["smsForm"]["message"].value;
// Validate the "to" field
if (to === "" || !/^\d{10}$/.test(to)) {
alert("Enter a valid 10-digit phone number.");
return false;
}
// Validate the "message" field
if (message === "" || message.length < 5) {
alert("Message should be at least 5 characters.");
return false;
}
return true;
}
</script>';
if ("" == $h) {
$outputForm .= ' <button class="btn btn-primary" onclick="goToLink()">Setup Credentials</button> <br>';
$outputForm .= '... or send a first message calling this URL: ';
$outputForm .= '<div style="display: flex; align-items: center;">';
$outputForm .= '<div style="margin-right: 10px;">';
$outputForm .= '<b><a href="' . $url . '?to=0123456789&message=Hello+world" target="_blank" data-toggle="tooltip" title="Send Your First SMS!"><i class="fas fa-link"></i> ' . $url . '?to=0123456789&message=Hello+world</a></b>';
$outputForm .= '</div>';
$outputForm .= '</div>';
} else {
$outputForm .= '... or send a direct message calling this URL: ';
$outputForm .= '<div style="display: flex; align-items: center;">';
$outputForm .= '<div style="margin-right: 10px;">';
$outputForm .= '<b><a href="' . $url . '?id=' . $id . '&h=' . $h . '&to=' . (("" != $to) ? $to : "0123456789") . '&message=' . urlencode(("" != $message) ? $message : "Hello world 🙂") . '" target="_blank" data-toggle="tooltip" title="Send Direct Message"><i class="fas fa-envelope"></i> ' . $url . '?id=' . $id . '&h=' . $h . '&to=' . (("" != $to) ? $to : "0123456789") . '&message=' . urlencode(("" != $message) ? $message : "Hello world 🙂") . '</a></b>';
$outputForm .= '</div>';
$outputForm .= '</div>';
}
$outputForm .= '</div>';
echo $outputForm;
PageRenderer::renderFooter();
//include 'footer.php';
echo ' </body> </html>';
}
elseif (!empty($to)) {
// Push the message on the server
$message_id = $smsgateway->sendMessage($device_id, $to, $message);
$outputSent = '
<body>
<div class="container mt-4">';
if (empty($message_id)) {
header('X-SMSGateway-State: FAILED');
$outputSent .= '<meta name="X-SMSGateway-State" content="0">
</head>
<body>';
} else {
header('X-SMSGateway-State: NEW');
header('X-SMSGateway-State-Url: ' . $url . '?id=' . $id . '&h=' . $h . '&mid=' . $message_id);
header('X-SMSGateway-Message-Id: ' . $message_id);
$outputSent .= '<meta name="X-SMSGateway-State" content="NEW">
<meta name="X-SMSGateway-State-Url" content="' . $url . '?id=' . $id . '&h=' . $h . '&mid=' . $message_id . '">
<meta name="X-SMSGateway-Message-Id" content="' . $message_id . '">
</head>
<body>';
}
//include Header
PageRenderer::renderHeader('Pulsebridge App');
// Display usage information
$outputSent .= '<div class="container mt-4">';
if (empty($messageId)) {
$outputSent .= '<div class="alert alert-success" role="alert">';
$outputSent .= 'Message : <strong>' . htmlspecialchars($message) . '</strong> to phone number : <strong> ' . htmlspecialchars($to) . '</strong> successfully pushed on the server.';
$outputSent .= '</div>';
$outputSent .= '<div class="alert alert-info" role="alert">';
$outputSent .= 'Device Code : <strong>' . htmlspecialchars($device_id) . '</strong> <br>Device Hash: <strong>' . htmlspecialchars($device_h) .'</strong>';
$outputSent .= '</div>';
} else {
$outputSent .= '<div class="alert alert-danger" role="alert">';
$outputSent .= 'Message <strong>' . htmlspecialchars($message) . '</strong> for ' . htmlspecialchars($to) . ' failed to push on the server.';
$outputSent .= '</div>';
}
$outputSent .= '<div class="row">';
$outputSent .= '<div class="col-md-6">';
$outputSent .= '<h2 class="h4 mb-3">Installation Instructions</h2>';
$outputSent .= '<p>If not done yet, please install the Android Pulsebridge App by clicking the link below: </p>';
$outputSent .= '<a href="https://github.com/medic/cht-gateway/releases/latest" target="_blank" class="btn btn-primary"><i class="fas fa-download"></i> Download SMSGatewayApp</a><br><br>';
$outputSent .= '<h2 class="h4 mb-3">App Configuration</h2>';
$outputSent .= '<p>Set the following URL in the Settings of the Android App:</p>';
$outputSent .= '<div class="input-group mb-3">';
$outputSent .= '<input type="text" class="form-control" id="app-url" value="' . htmlspecialchars($url) . '?id=' . htmlspecialchars($device_id) . '&h=' . htmlspecialchars($device_h) . '" readonly>';
$outputSent .= '<button class="btn btn-outline-info" type="button" onclick="copyToClipboard()">Copy URL</button>';
$outputSent .= '</div>';
$outputSent .= '<small class="text-muted">Click "Copy URL" to copy the configuration URL to the clipboard.</small><br>';
$outputSent .= '<small class="text-muted">Open Pulsebridge <a href="#">app</a> -> Settings -> Paste url to pulsebride url field</small>';
$outputSent .= '</div>';
$outputSent .= '<div class="col-md-6">';
$outputSent .= '<h2 class="h4 mb-3">Actions</h2>';
$outputSent .= '<p>Check SMS messages or send more SMS messages:</p>';
$outputSent .= '<a href="' . htmlspecialchars($url) . '?id=' . htmlspecialchars($device_id) . '&h=' . htmlspecialchars($device_h) . '&m" class="btn btn-success mb-2">Check SMS Messages <i class="bi bi-arrow-right"></i></a>';
$outputSent .= '<a href="' . htmlspecialchars($url) . '?id=' . htmlspecialchars($device_id) . '&h=' . htmlspecialchars($device_h) . '&to=&message=&i" class="btn btn-primary mb-2">Send More SMS Messages <i class="bi bi-arrow-right"></i></a>';
$outputSent .= '<h2 class="h4 mb-3">Dev Usage</h2>';
$outputSent .= '<p>Send Messages using HTTP GET on this URL from your application::</p>';
$outputSent .= '<div style="display: flex; align-items: center;">';
$outputSent .= '<div style="margin-right: 10px;">';
$outputSent .= '<b><a href="' . $url . '?id=' . htmlspecialchars($device_id) . '&h=' . htmlspecialchars($device_h) . '&to=' . (("" != $to) ? $to : "0123456789") . '&message=' . urlencode(("" != $message) ? $message : "Hello world 🙂") . '" target="_blank" data-toggle="tooltip" title="Send Direct Message"><i class="fas fa-envelope"></i> ' . $url . '?id=' . htmlspecialchars($device_id) . '&h=' . htmlspecialchars($device_h) . '&to=' . (("" != $to) ? $to : "0123456789") . '&message=' . urlencode(("" != $message) ? $message : "Your Message from pulsebridge🙂") . '</a></b>';
$outputSent .= '</div>';
$outputSent .= '</div>';
$outputSent .= '</div>';
$outputSent .= '</div>';
$outputSent .= '</div>';
$outputSent .= '<script>';
$outputSent .= 'function copyToClipboard() {';
$outputSent .= ' var input = document.getElementById("app-url");';
$outputSent .= ' input.select();';
$outputSent .= ' document.execCommand("copy");';
$outputSent .= ' var alertContainer = document.createElement("div");';
$outputSent .= ' alertContainer.style.padding = "10px";'; // Adjust the padding as needed
$outputSent .= ' alertContainer.style.position = "fixed";';
$outputSent .= ' alertContainer.style.top = "60px";'; // Adjust the top distance as needed
$outputSent .= ' alertContainer.style.right = "20px";'; // Adjust the right distance as needed
$outputSent .= ' alertContainer.style.zIndex = "1000";'; // Adjust the z-index as needed
$outputSent .= ' alertContainer.className = "alert-container";'; // Added a class for styling
$outputSent .= ' var alertDiv = document.createElement("div");';
$outputSent .= ' alertDiv.className = "alert alert-success alert-dismissible fade show";';
$outputSent .= ' alertDiv.innerHTML = "URL copied to clipboard!";';
$outputSent .= ' var closeButton = document.createElement("button");';
$outputSent .= ' closeButton.type = "button";';
$outputSent .= ' closeButton.className = "close";';
$outputSent .= ' closeButton.setAttribute("data-dismiss", "alert");';
$outputSent .= ' closeButton.innerHTML = "&times;";'; // "&times;" is the HTML entity for the close symbol (X)
$outputSent .= ' alertDiv.appendChild(closeButton);';
$outputSent .= ' alertContainer.appendChild(alertDiv);';
$outputSent .= ' document.body.appendChild(alertContainer);';
$outputSent .= ' setTimeout(function() {';
$outputSent .= ' alertContainer.remove();';
$outputSent .= ' }, 3000);'; // 3000 milliseconds = 3 seconds
$outputSent .= '}';
$outputSent .= '</script>';
echo $outputSent;
//include 'footer';
PageRenderer::renderFooter();
} else {
// Run the API server
$smsgateway->apiServer();
}

916
src/Pulsebridge.php Normal file
View file

@ -0,0 +1,916 @@
<?php
/**
* @file Pulsebridge.class.php
* @brief Pulsebridge - flat-file based SMS gateway
* PHP class using an open source Android app
*
* @mainpage
*
* Pulsebridge PHP class
*
* https://github.com/multiOTP/SMSGateway
*
* The Pulsebridge PHP class is a flat-file based SMS gateway for sending and
* receiving SMS on an Android device using an open source SMS Gateway app.
* (https://github.com/medic/cht-gateway)
*
* The Readme file contains additional information.
*
* PHP 5.3.0 or higher is supported.
*
* @author Amit Nandi (Bitmutex Technologies) <amit@bitmutex.com>
* @version 1.1.5
* @date 2023-09-20
* @since 2022-09-10
* @copyright (c) 2022-2024 Bitmutex Technologies
* @copyright Apache 2.0 License
*
*//*
*
* Usage
*
* Public methods available:
* apiServer($new_message_callback = "",
* $update_callback = "",
* $timeout_callback = "",
* $post_raw = "")
* archiveSuccessMessages($device_id = "")
* calculateAuthenticationHash($device_id)
* getDataPath()
* getDeviceFolder()
* getDeviceFolderArchive()
* getDeviceFolderLogs()
* getDeviceFolderReceive()
* getDeviceFolderSend()
* getDeviceId()
* getDevicePathArchive()
* getDevicePathLogs()
* getDevicePathReceive()
* getDevicePathSend()
* getDeviceTimeout()
* getMessagesToSend($device_id = "")
* getPurgeArchiveTime()
* getSuccessArchiveTime()
* getTimeoutDevices()
* getVersion()
* handleMessages($post_data)
* handleUpdates($post_data)
* purgeArchiveMessages($device_id = "")
* reactivatePushedMessages($pushed_timeout = 0)
* readAllMessages($device_id = "")
* readAllSentStatus($device_id = "")
* readAllArchivedStatus($device_id = "")
* readMessage($device_id = "", $message_id = "*", $message_filter = "*")
* readNewMessages($device_id = "")
* readSentStatus($device_id = "", $message_id = "*", $message_filter = "*")
* sendMessage($device_id, $to, $content)
* setDataPath($data_path)
* setDeviceFolder($device_folder)
* setDeviceId($device_id)
* setDeviceTimeout($device_timeout)
* setPurgeArchiveTime($purge_archive_time)
* setSuccessArchiveTime($success_archive_time)
* updateDataStructure($device_id)
* writeLog($log_message)
*
*
* Examples
*
* // Example 1 - Send message using Android phone with "demo" id
* use multiOTP\Pulsebridge\Pulsebridge;
* require_once('Pulsebridge.php');
* $smsgateway = new Pulsebridge();
* $smsgateway->setDataPath(__DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR);
* $smsgateway->setSharedSecret("secret");
* $device_id = "demo";
* $to = "+1234567890";
* $message = "Demo message";
* $device_h = $smsgateway->calculateAuthenticationHash($device_id);
* $message_id = $smsgateway->sendMessage($device_id, $to, $message);
* echo "Full URL for Android app URL: https://......./?id=$device_id&h=$device_h";
*
*
* // Example 2 - API server with call back function for new messages
* use multiOTP\Pulsebridge\Pulsebridge;
* require_once('Pulsebridge.php');
* function new_message_handling($array) {
* // Handling $array
* //[["device_id" => "device_id",
* // "message_id" => "message_id",
* // "from" => "from_phone",
* // "sms_sent" => "sms_sent_timestamp",
* // "sms_received" => "sms_received_timestamp",
* // "content" => "message_content",
* // "last_update" => "last update timestamp (ms)",
* // "status" => "message-status"
* // ],
* // [...]
* //]
* }
* $smsgateway = new Pulsebridge();
* $smsgateway->setDataPath(__DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR);
* $smsgateway->apiServer("new_message_handling");
*
*
* External device needed
*
* Android phone with SMS Gateway app installed
* (https://github.com/medic/cht-gateway/releases/latest)
*********************************************************************/
namespace Nmpl\Pulsebridge;
/**
* Pulsebridge - flat-file based SMS gateway PHP class using an open source Android app
*
* @author Andre Liechti (SysCo systemes de communication sa) <info@multiotp.net>
*/
require __DIR__ . '/../vendor/autoload.php';
class Pulsebridge
{
/**
* The Pulsebridge Version number.
*
* @var string
*/
const VERSION = '1.1.5';
/**
* The device timeout in seconds.
* Default of 5 minutes (300sec).
*
* @var int
*/
private $DeviceTimeout = 300;
/**
* The success archive time in seconds.
* Default of 1 day (1 * 86400 sec).
*
* @var int
*/
private $SuccessArchiveTime = 1 * 86400;
/**
* The purge archive time in seconds.
* Default of 90 days (90 * 86400 sec).
*
* @var int
*/
private $PurgeArchiveTime = 90 * 86400;
/**
* The purge log time in seconds.
* Default of 365 days (365 * 86400 sec).
*
* @var int
*/
private $PurgeLogTime = 365 * 86400;
/**
* The flat-file based data path (with terminal directory separator).
*
* @var string
*/
private $DataPath = '';
/**
* The Android device id.
*
* @var string
*/
private $DeviceId = '';
/**
* The shared secret to calculate hash authentication.
*
* @var string
*/
private $SharedSecret = 'secret';
/**
* The flat-file based device folder (without terminal directory separator).
*
* @var string
*/
private $DeviceFolder = '';
/**
* Class constructor.
*/
public function __construct()
{
// Define a default data path in the system temporary folder
$this->setDataPath(sys_get_temp_dir() . DIRECTORY_SEPARATOR);
}
/**
* Class destructor.
*/
public function __destruct()
{
// $this->...;
}
public function getVersion()
{
return self::VERSION;
}
private function getIPAddress() {
if(!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}
/**
* Set the flat-file data path
*
* @param string $data_path The flat-file data path (with terminal directory separator)
*
* @return bool true on success, false if folder is not available
*/
public function setDataPath(
$data_path
) {
if (file_exists($data_path)) {
if (substr($data_path, -strlen(DIRECTORY_SEPARATOR)) != DIRECTORY_SEPARATOR) {
$data_path.= DIRECTORY_SEPARATOR;
}
$this->DataPath = $data_path;
return true;
} else {
return false;
}
}
/**
* Get the flat-file data path
*
* @return string The flat-file data path (with terminal directory separator)
*/
public function getDataPath()
{
return $this->DataPath;
}
/**
* Set device id.
*
* @param string $device_id The Android device id (which is in the URL)
*/
public function setDeviceId(
$device_id
) {
$this->DeviceId = $device_id;
}
/**
* Get device id.
*
* @return string The Android device id (which is in the URL)
*/
public function getDeviceId()
{
return $this->DeviceId;
}
/**
* Set shared secret.
*
* @param string $shared_secret The shared secret to calculate hash authentication
*/
public function setSharedSecret(
$shared_secret
) {
$this->SharedSecret = $shared_secret;
}
/**
* Get shared secret.
*
* @return string The Android device id (which is in the URL)
*/
public function getSharedSecret()
{
return $this->SharedSecret;
}
public function setDeviceTimeout(
$device_timeout
) {
$this->DeviceTimeout = intval($device_timeout);
}
public function getDeviceTimeout()
{
return intval($this->DeviceTimeout);
}
public function setSuccessArchiveTime(
$success_archive_time
) {
$this->SuccessArchiveTime = intval($success_archive_time);
}
public function getSuccessArchiveTime()
{
return intval($this->SuccessArchiveTime);
}
public function setPurgeArchiveTime(
$purge_archive_time
) {
$this->PurgeArchiveTime = intval($purge_archive_time);
}
public function getPurgeArchiveTime()
{
return intval($this->PurgeArchiveTime);
}
public function setPurgeLogTime(
$purge_log_time
) {
$this->PurgeLogTime = intval($purge_log_time);
}
public function getPurgeLogTime()
{
return intval($this->PurgeLogTime);
}
public function setDeviceFolder(
$device_folder
) {
$this->DeviceFolder = $device_folder;
}
public function getDeviceFolder()
{
return $this->DeviceFolder;
}
public function getDeviceFolderLogs()
{
return $this->DeviceFolder . DIRECTORY_SEPARATOR . "logs";
}
public function getDeviceFolderSend()
{
return $this->DeviceFolder . DIRECTORY_SEPARATOR . "send";
}
public function getDeviceFolderReceive()
{
return $this->DeviceFolder . DIRECTORY_SEPARATOR . "receive";
}
public function getDeviceFolderArchive()
{
return $this->DeviceFolder . DIRECTORY_SEPARATOR . "archive";
}
public function getDevicePathLogs()
{
return $this->getDeviceFolderLogs() . DIRECTORY_SEPARATOR;
}
public function getDevicePathSend()
{
return $this->getDeviceFolderSend() . DIRECTORY_SEPARATOR;
}
public function getDevicePathReceive()
{
return $this->getDeviceFolderReceive() . DIRECTORY_SEPARATOR;
}
public function getDevicePathArchive()
{
return $this->getDeviceFolderArchive() . DIRECTORY_SEPARATOR;
}
public function handleMessages(
$post_data
) {
$result_array = array();
$extract_data = json_decode(str_replace(chr(13), "", $post_data), true);
if (null != $extract_data) {
if (isset($extract_data["messages"])) {
foreach($extract_data["messages"] as $message) {
if (isset($message["id"])) {
$from = (isset($message["from"]) ? $message["from"] : "");
$sms_sent = (isset($message["sms_sent"]) ? $message["sms_sent"] : "");
$sms_received = (isset($message["sms_received"]) ? $message["sms_received"] : "");
$content = (isset($message["content"]) ? $message["content"] : "");
$message_data = "from:$from\n";
$message_data.= "sms_sent:$sms_sent\n";
$message_data.= "sms_received:$sms_received\n";
$message_data.= $content;
file_put_contents($this->getDevicePathReceive() . $message["id"] . ".UNREAD", $message_data);
array_push($result_array, ["device_id" => $this->getDeviceId(),
"message_id" => $message["id"],
"from" => $from,
"sms_sent" => $sms_sent,
"sms_received" => $sms_received,
"content" => $content,
"last_update" => time() . "000",
"status" => "UNREAD"
]);
}
}
}
}
return $result_array;
}
public function handleUpdates(
$post_data
) {
$result_array = array();
$extract_data = json_decode(str_replace(chr(13), "", $post_data), true);
if (null != $extract_data) {
if (isset($extract_data["updates"])) {
foreach($extract_data["updates"] as $update) {
if (isset($update["id"])) {
if (isset($update["status"])) {
$message_array = glob($this->getDevicePathSend() . $update["id"] . ".*");
if (1 == count($message_array)) {
$extract_data = json_decode(str_replace(chr(13), "", file_get_contents($message_array[0])), true);
$content = "";
$to = "";
if (null != $extract_data) {
$content = isset($extract_data["content"]) ? $extract_data["content"] : "";
$to = isset($extract_data["to"]) ? $extract_data["to"] : "";
}
array_push($result_array, ["device_id" => $this->getDeviceId(),
"message_id" => $update["id"],
"to" => $to,
"content" => $content,
"last_update" => filemtime($message_array[0]) . "000",
"status" => $update["status"]
]);
$updated_message = $this->getDevicePathSend() . $update["id"] . "." . $update["status"];
rename($message_array[0], $updated_message);
touch($updated_message);
}
}
}
}
}
}
return $result_array;
}
public function updateDataStructure(
$device_id,
$touch = true
) {
$result = false;
$this->setDeviceId($device_id);
if (file_exists($this->getDataPath()) && (!empty($device_id))) {
$this->setDeviceFolder($this->getDataPath() . $this->getDeviceId());
if (!file_exists($this->getDeviceFolder())) {
mkdir($this->getDeviceFolder());
}
if ($touch) {
touch($this->getDeviceFolder());
}
if (!file_exists($this->getDeviceFolderLogs())) {
mkdir($this->getDeviceFolderLogs());
}
if (!file_exists($this->getDeviceFolderSend())) {
mkdir($this->getDeviceFolderSend());
}
if (!file_exists($this->getDeviceFolderReceive())) {
mkdir($this->getDeviceFolderReceive());
}
if (!file_exists($this->getDeviceFolderArchive())) {
mkdir($this->getDeviceFolderArchive());
}
$result = true;
}
return $result;
}
public function readNewMessages(
$device_id = ""
) {
return $this->readMessage($device_id, "*", "UNREAD");
}
public function readAllMessages(
$device_id = ""
) {
return $this->readMessage($device_id);
}
public function readMessage(
$device_id = "",
$message_id = "*",
$message_filter = "*"
) {
$result_array = array();
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if ($this->updateDataStructure($device_id)) {
$messages_new_array = glob($this->getDevicePathReceive() . "$message_id.$message_filter");
// Sort based on time, last update on the top
usort($messages_new_array, function($a,$b){ return filemtime($b) - filemtime($a);});
if (count($messages_new_array) > 0) {
foreach($messages_new_array as $message) {
$from = "";
$sms_sent = "";
$sms_received = "";
$content = "";
$line_count = 0;
$file = fopen($message, "r");
while(! feof($file)) {
$line_count++;
$line_content = fgets($file);
if (1 == $line_count) {
$from = str_replace("from:", "", $line_content);
} elseif (2 == $line_count) {
$sms_sent = str_replace("sms_sent:", "", $line_content);
} elseif (3 == $line_count) {
$sms_received = str_replace("sms_received:", "", $line_content);
} else {
if ($line_count > 4) {
$content.= "\n";
}
$content.= $line_content;
}
}
fclose($file);
array_push($result_array, ["device_id" => $device_id,
"message_id" => pathinfo($message)['filename'],
"from" => $from,
"sms_sent" => $sms_sent,
"sms_received" => $sms_received,
"content" => $content,
"last_update" => filemtime($message) . "000",
"status" => pathinfo($message)['extension']
]);
$message_read = str_replace(".UNREAD", ".READ", $message);
if ($message_read != $message) {
rename($message, $message_read);
touch($message_read);
}
}
}
}
return $result_array;
}
public function readAllArchivedStatus(
$device_id = ""
) {
return $this->readSentStatus($device_id, "*", "*", $this->getDevicePathArchive());
}
public function readAllSentStatus(
$device_id = ""
) {
return $this->readSentStatus($device_id);
}
public function readSentStatus(
$device_id = "",
$message_id = "*",
$message_filter = "*",
$message_folder = ""
) {
$result_array = array();
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if (empty($message_folder)) {
$message_folder = $this->getDevicePathSend();
}
if ($this->updateDataStructure($device_id)) {
$messages_new_array = glob($message_folder . "$message_id.$message_filter");
// Sort based on time, last update on the top
usort($messages_new_array, function($a,$b){ return filemtime($b) - filemtime($a);});
if (count($messages_new_array) > 0) {
foreach($messages_new_array as $message) {
$id = pathinfo($message)['filename'];
$status = pathinfo($message)['extension'];
$extract_data = json_decode(str_replace(chr(13), "", file_get_contents($message)), true);
$content = "";
$to = "";
if (null != $extract_data) {
$content = isset($extract_data["content"]) ? $extract_data["content"] : "";
$to = isset($extract_data["to"]) ? $extract_data["to"] : "";
} else {
$content = "DEBUG: ".json_last_error_msg();
}
array_push($result_array, ["device_id" => $device_id,
"message_id" => $id,
"to" => $to,
"content" => $content,
"last_update" => filemtime($message) . "000",
"status" => $status
]);
}
}
}
return $result_array;
}
public function sendMessage(
$device_id,
$to,
$content
) {
$message_id = "";
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if ($this->updateDataStructure($device_id)) {
$escape_content = addcslashes($content, "\\\"\n");
$message_id = str_replace(".","-", uniqid("", true));
$message_content = "{\"id\": \"$message_id\", \"to\": \"$to\", \"content\": \"$escape_content\"}";
file_put_contents($this->getDevicePathSend() . $message_id . ".NEW", $message_content);
}
return $message_id;
}
public function reactivatePushedMessages(
$pushed_timeout = 0
) {
$messages_count = 0;
if ($pushed_timeout > 0) {
$messages_pushed_array = glob($this->getDevicePathSend() . "*.PUSHED");
foreach($messages_pushed_array as $message_pushed) {
if (time() > (filemtime($message_pushed) + $pushed_timeout)) {
$message_new = str_replace(".PUSHED", ".NEW", $message_pushed);
rename($message_pushed, $message_new);
touch($message_new);
$messages_count++;
}
}
}
return $messages_count;
}
public function getMessagesToSend(
$device_id = ""
) {
$result = "{";
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if ($this->updateDataStructure($device_id)) {
$messages_new_array = glob($this->getDevicePathSend() . "*.NEW");
// Sort based on time, older on top
usort($messages_new_array, function($a,$b){ return filemtime($a) - filemtime($b);});
if (count($messages_new_array) > 0) {
$result.= "\"messages\": [";
foreach($messages_new_array as $message_new) {
$result.= file_get_contents($message_new) . ",";
$message_pushed = str_replace(".NEW", ".PUSHED", $message_new);
rename($message_new, $message_pushed);
touch($message_pushed);
}
$result = substr($result, 0, (strlen($result) - 1)) . "]";
}
}
$result.= "}";
return $result;
}
public function getTimeoutDevices()
{
$result_array = array();
if ($this->getDeviceTimeout() > 0) {
$devices_array = glob($this->getDataPath() . "*");
// Sort based on time, older on top
usort($devices_array, function($a,$b){ return filemtime($a) - filemtime($b);});
foreach($devices_array as $device) {
if (is_dir($device) && ("." != $device) && (".." != $device)) {
$last_update = filemtime($device);
if (time() > ($last_update + $this->getDeviceTimeout())) {
array_push($result_array, ["device_id" => pathinfo($device)['basename'],
"last_update" => $last_update
]);
}
}
}
}
return $result_array;
}
/**
* Archive successful messages, which are
* DELIVERED sent messages and READ received messages
*
* @param string $device_id The Android device id (which is in the URL)
*
* @return int The number of messages archived
*/
public function archiveSuccessMessages(
$device_id = ""
) {
$archived_messages = 0;
if ($this->getSuccessArchiveTime() > 0) {
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if ($this->updateDataStructure($device_id)) {
$messages_array = glob($this->getDevicePathSend() . "*.DELIVERED");
foreach($messages_array as $message) {
if (time() > (filemtime($message) + $this->getSuccessArchiveTime())) {
$message_archive = str_replace($this->getDevicePathSend(), $this->getDevicePathArchive(), $message);
rename($message, $message_archive);
$archived_messages++;
}
}
$messages_array = glob($this->getDevicePathReceive() . "*.READ");
foreach($messages_array as $message) {
if (time() > (filemtime($message) + $this->getSuccessArchiveTime())) {
$message_archive = str_replace($this->getDevicePathReceive(), $this->getDevicePathArchive(), $message);
rename($message, $message_archive);
$archived_messages++;
}
}
}
}
return $archived_messages;
}
/**
* Purge archived messages
*
* @param string $device_id The Android device id (which is in the URL)
*
* @return int The number of messages purged
*/
public function purgeArchiveMessages(
$device_id = ""
) {
$purged_messages = 0;
if ($this->getPurgeArchiveTime() > 0) {
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if ($this->updateDataStructure($device_id)) {
$messages_array = glob($this->getDevicePathArchive() . "*.*");
foreach($messages_array as $message) {
if (is_file($message) && (time() > (filemtime($message) + $this->getPurgeArchiveTime()))) {
unlink($message);
$purged_messages++;
}
}
}
}
return $purged_messages;
}
/**
* Purge log files
*
* @param string $device_id The Android device id (which is in the URL)
*
* @return int The number of messages purged
*/
public function purgeLogFiles(
$device_id = ""
) {
$purged_files = 0;
if ($this->getPurgeLogTime() > 0) {
if (empty($device_id)) {
$device_id = $this->getDeviceId();
}
if ($this->updateDataStructure($device_id)) {
$log_array = glob($this->getDevicePathLogs() . "*.log");
foreach($log_array as $log_file) {
if (is_file($log_file) && (time() > (filemtime($log_file) + $this->getPurgeLogTime()))) {
unlink($log_file);
$purged_files++;
}
}
}
}
return $purged_files;
}
public function writeLog(
$log_message
) {
$result = false;
if (("" != $this->getDeviceFolder()) && file_exists($this->getDeviceFolderLogs())) {
file_put_contents($this->getDevicePathLogs() . date("Y-m-d").".log",
date("Y-m-d H:i:s") . " " . $this->getIPAddress() . " " . $log_message."\n",
FILE_APPEND
);
$result = true;
}
return $result;
}
public function calculateAuthenticationHash(
$device_id
) {
return substr(strtolower(md5($this->getSharedSecret() . "#salt@" . $device_id)), 0, 6);
}
/**
* Main API server, which displays directly the necessary information
*
* @param string $new_message_callback Callback action for new message
* @param string $update_callback Callback action for updated status
* @param string $timeout_callback Callback action for timeout detection
* @param string $post_raw Forced raw post data (mainly for debugging and tests)
*/
public function apiServer(
$new_message_callback = NULL,
$update_callback = NULL,
$timeout_callback = NULL,
$post_raw = ""
) {
$response_code = 200;
$response = "";
$device_id = isset($_GET["id"]) ? $_GET["id"] : '';
$device_h = isset($_GET["h"]) ? $_GET["h"] : '';
if ("" == $device_id) {
$response_code = 404;
} elseif (($device_h != $this->calculateAuthenticationHash($device_id))) {
$response_code = 401;
$device_id = "";
}
if ($this->updateDataStructure($device_id)) {
//Json Response
header('Content-Type: application/json');
//ob_end_clean();
$response = json_encode(["pulsebridge-gateway" => true]); // API Bridge Word
//manually json encoded response
// $response = "{\"pulsebridge-gateway\": true}"; // API Bridge Word
if (!empty($post_raw)) {
$post_data = $post_raw;
} else {
$post_data = file_get_contents("php://input");
}
if (!empty($post_data)) {
$this->writeLog($post_data);
$new_messages_array = $this->handleMessages($post_data);
$updates_array = $this->handleUpdates($post_data);
$this->reactivatePushedMessages($this->getDeviceTimeout());
$response = $this->getMessagesToSend();
if (is_callable($new_message_callback)) {
if (count($new_messages_array) > 0) {
$new_message_callback($new_messages_array);
foreach($new_messages_array as $message) {
$this->readMessage("", $message["message_id"]);
}
}
}
if (is_callable($update_callback)) {
if (count($updates_array) > 0) {
$update_callback($updates_array);
}
}
}
// Ordering and cleaning for the current device id
$this->archiveSuccessMessages();
$this->purgeArchiveMessages();
$this->purgeLogFiles();
}
if (is_callable($timeout_callback)) {
if ($this->getDeviceTimeout() > 0) {
$timeout_callback($this->getTimeoutDevices());
}
}
http_response_code($response_code);
echo $response;
}
}

View file

@ -0,0 +1,31 @@
<?php
use Nmpl\Pulsebridge\Logger;
use Nmpl\Pulsebridge\PageRenderer;
use Nmpl\Pulsebridge\Pulsebridge;
use PHPUnit\Framework\TestCase;
class PageRendererTest extends TestCase
{
public function testConstructor()
{
// Mock objects for Pulsebridge and Logger
$pulsebridgeMock = $this->createMock(Pulsebridge::class);
$loggerMock = $this->createMock(Logger::class);
// Expect the setDataPath method to be called on Pulsebridge
$pulsebridgeMock->expects($this->once())
->method('setDataPath');
// Expect the log method to be called on Logger
$loggerMock->expects($this->once())
->method('log')
->with($this->equalTo('Renderer Instantiated'));
// Create PageRenderer instance with mock objects
$renderer = new PageRenderer($pulsebridgeMock, $loggerMock);
}
}

View file

@ -0,0 +1,50 @@
<?php
use PHPUnit\Framework\TestCase;
use Nmpl\Pulsebridge\Pulsebridge;
class PulsebridgeTest extends TestCase
{
private $smsgateway;
protected function setUp(): void
{
$this->smsgateway = new Pulsebridge();
$this->smsgateway->setDataPath(__DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR);
$this->smsgateway->setSharedSecret("secret");
}
public function testInitialization()
{
$this->assertInstanceOf(Pulsebridge::class, $this->smsgateway);
}
public function testSetAndGetDeviceId()
{
$deviceId = "testDevice";
$this->smsgateway->setDeviceId($deviceId);
$this->assertEquals($deviceId, $this->smsgateway->getDeviceId());
}
public function testSetAndGetSharedSecret()
{
$sharedSecret = "newSecret";
$this->smsgateway->setSharedSecret($sharedSecret);
$this->assertEquals($sharedSecret, $this->smsgateway->getSharedSecret());
}
// Add tests for other getters and setters...
public function testSendMessage()
{
$to = "+1234567890";
$content = "Test message content";
$messageId = $this->smsgateway->sendMessage("testDevice", $to, $content);
// Assert that the message ID is not empty and follows the expected format
$this->assertNotEmpty($messageId);
$this->assertMatchesRegularExpression('/^[a-zA-Z0-9-]+$/', $messageId);
// Add more assertions for the sent message...
}
}

BIN
webfonts/fa-solid-900.ttf Normal file

Binary file not shown.

BIN
webfonts/fa-solid-900.woff2 Normal file

Binary file not shown.