Robert 2 years ago
commit 2b5b9efb93
  1. 4
      .gitignore
  2. 42
      protected/README.MD
  3. 5
      protected/composer.json
  4. 314
      protected/src/classes/Article.php
  5. 161
      protected/src/classes/Category.php
  6. 63
      protected/src/config.php
  7. 28
      protected/src/tables.sql
  8. 197
      protected/src/templates/admin/editArticle.php
  9. 38
      protected/src/templates/admin/editCategory.php
  10. 4
      protected/src/templates/admin/include/header.php
  11. 46
      protected/src/templates/admin/listArticles.php
  12. 36
      protected/src/templates/admin/listCategories.php
  13. 30
      protected/src/templates/admin/loginForm.php
  14. 35
      protected/src/templates/archive.php
  15. 34
      protected/src/templates/homepage.php
  16. 7
      protected/src/templates/include/footer.php
  17. 12
      protected/src/templates/include/header.php
  18. 19
      protected/src/templates/viewArticle.php
  19. 304
      public/admin.php
  20. 391
      public/assets/css/style.css
  21. BIN
      public/assets/images/logo.jpg
  22. BIN
      public/assets/images/logo.png
  23. 39
      public/assets/js/ckeditor.d.ts
  24. 7
      public/assets/js/ckeditor.js
  25. 1
      public/assets/js/ckeditor.js.map
  26. 45
      public/image.php
  27. 56
      public/index.php
  28. 78
      public/upload.php

4
.gitignore vendored

@ -0,0 +1,4 @@
.env
protected/vendor/
protected/composer.lock
protected/images/

@ -0,0 +1,42 @@
# cms
Original Author: Matt Doyle
Updates: Robert S.
## SETUP from MySQL Root run:
```
Change this password SJ6G*WyaV7PvvEts@vxjm below!
$ mysql -u root -p
> CREATE USER 'zoombox'@'localhost' IDENTIFIED BY 'SJ6G*WyaV7PvvEts@vxjm';
> GRANT ALL ON cms.* TO 'zoombox'@'localhost';
> create database cms;
```
## Import the tables.sql file:
```
$ mysql -u cms -p cms < tables.sql
enter this password when prompted: SJ6G*WyaV7PvvEts@vxjm
```
The config.php file is in the protected/src folder.
```
define( "BLOG_NAME", "Widgetz Newz" ); // Display Name for Titles
```
## Create the .env secrets file
```
$nano .env
DB_TYPE=mysql
DB_HOST=127.0.0.1
DB_NAME=cms
DB_USERNAME=zoombox
DB_PASSWORD=SJ6G*WyaV7PvvEts@vxjm
ADMIN_USERS=zug:SunSet@XQWET,zig:546Poker@xzyWhy
```
## PHP Deps
```
PHP gd and mysql must be installed:
$ sudo apt-get install php8.3-gd
$ sudo apt-get install php8.3-mysql
```

@ -0,0 +1,5 @@
{
"require": {
"vlucas/phpdotenv": "^5.6"
}
}

@ -0,0 +1,314 @@
<?php
/**
* Class to handle articles
*/
class Article
{
// Properties
/**
* @var int The article ID from the database
*/
public $id = null;
/**
* @var int When the article is to be / was first published
*/
public $publicationDate = null;
/**
* @var int The article category ID
*/
public $categoryId = null;
/**
* @var string Full title of the article
*/
public $title = null;
/**
* @var string A short summary of the article
*/
public $summary = null;
/**
* @var string The HTML content of the article
*/
public $content = null;
/**
* @var string The filename extension of the article's full-size and thumbnail images (empty string means the article has no image)
*/
public $imageExtension = "";
/**
* Sets the object's properties using the values in the supplied array
*
* @param assoc The property values
*/
public function __construct( $data=array() ) {
if ( isset( $data['id'] ) ) $this->id = (int) $data['id'];
if ( isset( $data['publicationDate'] ) ) $this->publicationDate = (int) $data['publicationDate'];
if ( isset( $data['categoryId'] ) ) $this->categoryId = (int) $data['categoryId'];
if ( isset( $data['title'] ) ) $this->title = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['title'] );
if ( isset( $data['summary'] ) ) $this->summary = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['summary'] );
if ( isset( $data['content'] ) ) $this->content = $data['content'];
if ( isset( $data['imageExtension'] ) ) $this->imageExtension = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\$ a-zA-Z0-9()]/", "", $data['imageExtension'] );
}
/**
* Sets the object's properties using the edit form post values in the supplied array
*
* @param assoc The form post values
*/
public function storeFormValues( $params ) {
// Store all the parameters
$this->__construct( $params );
// Parse and store the publication date
if ( isset($params['publicationDate']) ) {
$publicationDate = explode ( '-', $params['publicationDate'] );
if ( count($publicationDate) == 3 ) {
list ( $y, $m, $d ) = $publicationDate;
$this->publicationDate = mktime ( 0, 0, 0, $m, $d, $y );
}
}
}
/**
* Stores any image uploaded from the edit form
*
* @param assoc The 'image' element from the $_FILES array containing the file upload data
*/
public function storeUploadedImage( $image ) {
if ( $image['error'] == UPLOAD_ERR_OK )
{
// Does the Article object have an ID?
if ( is_null( $this->id ) ) trigger_error( "Article::storeUploadedImage(): Attempt to upload an image for an Article object that does not have its ID property set.", E_USER_ERROR );
// Delete any previous image(s) for this article
$this->deleteImages();
// Get and store the image filename extension
$this->imageExtension = strtolower( strrchr( $image['name'], '.' ) );
// Store the image
$tempFilename = trim( $image['tmp_name'] );
if ( is_uploaded_file ( $tempFilename ) ) {
if ( !( move_uploaded_file( $tempFilename, $this->getImagePath() ) ) ) trigger_error( "Article::storeUploadedImage(): Couldn't move uploaded file.", E_USER_ERROR );
if ( !( chmod( $this->getImagePath(), 0664 ) ) ) trigger_error( "Article::storeUploadedImage(): Couldn't set permissions on uploaded file.", E_USER_ERROR );
}
// Get the image size and type
$attrs = getimagesize ( $this->getImagePath() );
$imageWidth = $attrs[0];
$imageHeight = $attrs[1];
$imageType = $attrs[2];
// Load the image into memory
switch ( $imageType ) {
case IMAGETYPE_GIF:
$imageResource = imagecreatefromgif ( $this->getImagePath() );
break;
case IMAGETYPE_JPEG:
$imageResource = imagecreatefromjpeg ( $this->getImagePath() );
break;
case IMAGETYPE_PNG:
$imageResource = imagecreatefrompng ( $this->getImagePath() );
break;
default:
trigger_error ( "Article::storeUploadedImage(): Unhandled or unknown image type ($imageType)", E_USER_ERROR );
}
// Copy and resize the image to create the thumbnail
$thumbHeight = intval ( $imageHeight / $imageWidth * ARTICLE_THUMB_WIDTH );
$thumbResource = imagecreatetruecolor ( ARTICLE_THUMB_WIDTH, $thumbHeight );
imagecopyresampled( $thumbResource, $imageResource, 0, 0, 0, 0, ARTICLE_THUMB_WIDTH, $thumbHeight, $imageWidth, $imageHeight );
// Save the thumbnail
switch ( $imageType ) {
case IMAGETYPE_GIF:
imagegif ( $thumbResource, $this->getImagePath( IMG_TYPE_THUMB ) );
break;
case IMAGETYPE_JPEG:
imagejpeg ( $thumbResource, $this->getImagePath( IMG_TYPE_THUMB ), JPEG_QUALITY );
break;
case IMAGETYPE_PNG:
imagepng ( $thumbResource, $this->getImagePath( IMG_TYPE_THUMB ) );
break;
default:
trigger_error ( "Article::storeUploadedImage(): Unhandled or unknown image type ($imageType)", E_USER_ERROR );
}
$this->update();
}
}
/**
* Deletes any images and/or thumbnails associated with the article
*/
public function deleteImages() {
// Delete all fullsize images for this article
foreach (glob( ARTICLE_IMAGE_PATH . "/" . IMG_TYPE_FULLSIZE . "/" . $this->id . ".*") as $filename) {
if ( !unlink( $filename ) ) trigger_error( "Article::deleteImages(): Couldn't delete image file.", E_USER_ERROR );
}
// Delete all thumbnail images for this article
foreach (glob( ARTICLE_IMAGE_PATH . "/" . IMG_TYPE_THUMB . "/" . $this->id . ".*") as $filename) {
if ( !unlink( $filename ) ) trigger_error( "Article::deleteImages(): Couldn't delete thumbnail file.", E_USER_ERROR );
}
// Remove the image filename extension from the object
$this->imageExtension = "";
}
/**
* Returns the relative path to the article's full-size or thumbnail image
*
* @param string The type of image path to retrieve (IMG_TYPE_FULLSIZE or IMG_TYPE_THUMB). Defaults to IMG_TYPE_FULLSIZE.
* @return string|false The image's path, or false if an image hasn't been uploaded
*/
public function getImagePath( $type=IMG_TYPE_FULLSIZE ) {
return ( $this->id && $this->imageExtension ) ? ( SITE_URL . "/image.php?rel={$type}&image=" . $this->id . $this->imageExtension ) : false;
}
/**
* Returns an Article object matching the given article ID
*
* @param int The article ID
* @return Article|false The article object, or false if the record was not found or there was a problem
*/
public static function getById( $id ) {
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "SELECT *, UNIX_TIMESTAMP(publicationDate) AS publicationDate FROM articles WHERE id = :id";
$st = $conn->prepare( $sql );
$st->bindValue( ":id", $id, PDO::PARAM_INT );
$st->execute();
$row = $st->fetch();
$conn = null;
if ( $row ) return new Article( $row );
}
/**
* Returns all (or a range of) Article objects in the DB
*
* @param int Optional The number of rows to return (default=all)
* @param int Optional Return just articles in the category with this ID
* @return Array|false A two-element array : results => array, a list of Article objects; totalRows => Total number of articles
*/
public static function getList( $numRows=1000000, $categoryId=null ) {
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$categoryClause = $categoryId ? "WHERE categoryId = :categoryId" : "";
$sql = "SELECT SQL_CALC_FOUND_ROWS *, UNIX_TIMESTAMP(publicationDate) AS publicationDate
FROM articles $categoryClause
ORDER BY publicationDate DESC LIMIT :numRows";
$st = $conn->prepare( $sql );
$st->bindValue( ":numRows", $numRows, PDO::PARAM_INT );
if ( $categoryId ) $st->bindValue( ":categoryId", $categoryId, PDO::PARAM_INT );
$st->execute();
$list = array();
while ( $row = $st->fetch() ) {
$article = new Article( $row );
$list[] = $article;
}
// Now get the total number of articles that matched the criteria
$sql = "SELECT FOUND_ROWS() AS totalRows";
$totalRows = $conn->query( $sql )->fetch();
$conn = null;
return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) );
}
/**
* Inserts the current Article object into the database, and sets its ID property.
*/
public function insert() {
// Does the Article object already have an ID?
if ( !is_null( $this->id ) ) trigger_error ( "Article::insert(): Attempt to insert an Article object that already has its ID property set (to $this->id).", E_USER_ERROR );
// Insert the Article
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "INSERT INTO articles ( publicationDate, categoryId, title, summary, content, imageExtension ) VALUES ( FROM_UNIXTIME(:publicationDate), :categoryId, :title, :summary, :content, :imageExtension )";
$st = $conn->prepare ( $sql );
$st->bindValue( ":publicationDate", $this->publicationDate, PDO::PARAM_INT );
$st->bindValue( ":categoryId", $this->categoryId, PDO::PARAM_INT );
$st->bindValue( ":title", $this->title, PDO::PARAM_STR );
$st->bindValue( ":summary", $this->summary, PDO::PARAM_STR );
$st->bindValue( ":content", $this->content, PDO::PARAM_STR );
$st->bindValue( ":imageExtension", $this->imageExtension, PDO::PARAM_STR );
$st->execute();
$this->id = $conn->lastInsertId();
$conn = null;
}
/**
* Updates the current Article object in the database.
*/
public function update() {
// Does the Article object have an ID?
if ( is_null( $this->id ) ) trigger_error ( "Article::update(): Attempt to update an Article object that does not have its ID property set.", E_USER_ERROR );
// Update the Article
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "UPDATE articles SET publicationDate=FROM_UNIXTIME(:publicationDate), categoryId=:categoryId, title=:title, summary=:summary, content=:content, imageExtension=:imageExtension WHERE id = :id";
$st = $conn->prepare ( $sql );
$st->bindValue( ":publicationDate", $this->publicationDate, PDO::PARAM_INT );
$st->bindValue( ":categoryId", $this->categoryId, PDO::PARAM_INT );
$st->bindValue( ":title", $this->title, PDO::PARAM_STR );
$st->bindValue( ":summary", $this->summary, PDO::PARAM_STR );
$st->bindValue( ":content", $this->content, PDO::PARAM_STR );
$st->bindValue( ":imageExtension", $this->imageExtension, PDO::PARAM_STR );
$st->bindValue( ":id", $this->id, PDO::PARAM_INT );
$st->execute();
$conn = null;
}
/**
* Deletes the current Article object from the database.
*/
public function delete() {
// Does the Article object have an ID?
if ( is_null( $this->id ) ) trigger_error ( "Article::delete(): Attempt to delete an Article object that does not have its ID property set.", E_USER_ERROR );
// Delete the Article
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$st = $conn->prepare ( "DELETE FROM articles WHERE id = :id LIMIT 1" );
$st->bindValue( ":id", $this->id, PDO::PARAM_INT );
$st->execute();
$conn = null;
}
}

@ -0,0 +1,161 @@
<?php
/**
* Class to handle article categories
*/
class Category
{
// Properties
/**
* @var int The category ID from the database
*/
public $id = null;
/**
* @var string Name of the category
*/
public $name = null;
/**
* @var string A short description of the category
*/
public $description = null;
/**
* Sets the object's properties using the values in the supplied array
*
* @param assoc The property values
*/
public function __construct( $data=array() ) {
if ( isset( $data['id'] ) ) $this->id = (int) $data['id'];
if ( isset( $data['name'] ) ) $this->name = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['name'] );
if ( isset( $data['description'] ) ) $this->description = preg_replace ( "/[^\.\,\-\_\'\"\@\?\!\:\$ a-zA-Z0-9()]/", "", $data['description'] );
}
/**
* Sets the object's properties using the edit form post values in the supplied array
*
* @param assoc The form post values
*/
public function storeFormValues ( $params ) {
// Store all the parameters
$this->__construct( $params );
}
/**
* Returns a Category object matching the given category ID
*
* @param int The category ID
* @return Category|false The category object, or false if the record was not found or there was a problem
*/
public static function getById( $id ) {
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "SELECT * FROM categories WHERE id = :id";
$st = $conn->prepare( $sql );
$st->bindValue( ":id", $id, PDO::PARAM_INT );
$st->execute();
$row = $st->fetch();
$conn = null;
if ( $row ) return new Category( $row );
}
/**
* Returns all (or a range of) Category objects in the DB
*
* @param int Optional The number of rows to return (default=all)
* @return Array|false A two-element array : results => array, a list of Category objects; totalRows => Total number of categories
*/
public static function getList( $numRows=1000000 ) {
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "SELECT SQL_CALC_FOUND_ROWS * FROM categories
ORDER BY name ASC LIMIT :numRows";
$st = $conn->prepare( $sql );
$st->bindValue( ":numRows", $numRows, PDO::PARAM_INT );
$st->execute();
$list = array();
while ( $row = $st->fetch() ) {
$category = new Category( $row );
$list[] = $category;
}
// Now get the total number of categories that matched the criteria
$sql = "SELECT FOUND_ROWS() AS totalRows";
$totalRows = $conn->query( $sql )->fetch();
$conn = null;
return ( array ( "results" => $list, "totalRows" => $totalRows[0] ) );
}
/**
* Inserts the current Category object into the database, and sets its ID property.
*/
public function insert() {
// Does the Category object already have an ID?
if ( !is_null( $this->id ) ) trigger_error ( "Category::insert(): Attempt to insert a Category object that already has its ID property set (to $this->id).", E_USER_ERROR );
// Insert the Category
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "INSERT INTO categories ( name, description ) VALUES ( :name, :description )";
$st = $conn->prepare ( $sql );
$st->bindValue( ":name", $this->name, PDO::PARAM_STR );
$st->bindValue( ":description", $this->description, PDO::PARAM_STR );
$st->execute();
$this->id = $conn->lastInsertId();
$conn = null;
}
/**
* Updates the current Category object in the database.
*/
public function update() {
// Does the Category object have an ID?
if ( is_null( $this->id ) ) trigger_error ( "Category::update(): Attempt to update a Category object that does not have its ID property set.", E_USER_ERROR );
// Update the Category
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$sql = "UPDATE categories SET name=:name, description=:description WHERE id = :id";
$st = $conn->prepare ( $sql );
$st->bindValue( ":name", $this->name, PDO::PARAM_STR );
$st->bindValue( ":description", $this->description, PDO::PARAM_STR );
$st->bindValue( ":id", $this->id, PDO::PARAM_INT );
$st->execute();
$conn = null;
}
/**
* Deletes the current Category object from the database.
*/
public function delete() {
// Does the Category object have an ID?
if ( is_null( $this->id ) ) trigger_error ( "Category::delete(): Attempt to delete a Category object that does not have its ID property set.", E_USER_ERROR );
// Delete the Category
$conn = new PDO( DB_DSN, DB_USERNAME, DB_PASSWORD );
$st = $conn->prepare ( "DELETE FROM categories WHERE id = :id LIMIT 1" );
$st->bindValue( ":id", $this->id, PDO::PARAM_INT );
$st->execute();
$conn = null;
}
}

@ -0,0 +1,63 @@
<?php
/**
* @link https://www.elated.com/cms-in-an-afternoon-php-mysql/
* https://www.elated.com/add-image-uploading-to-your-cms/
* https://www.elated.com/mod-rewrite-tutorial-for-absolute-beginners/#friendly-urls
*/
define( "UPLOAD_RND_FILE_NAMES", false ); // Randomized file names to allow Duplicates?
define( "BLOG_NAME", "Widgetz Newz" ); // Display Name for Titles
define( "HOMEPAGE_NUM_ARTICLES", 5 );
ini_set( "display_errors", false );
date_default_timezone_set( "America/Detroit" ); // http://www.php.net/manual/en/timezones.php
$d = dirname(__DIR__, 1); // Up one level for Composer Vendor DIR
require_once $d . '/vendor/autoload.php';
$e = dirname(__DIR__,2); // Up two levels for ENV file
$dotenv = Dotenv\Dotenv::createImmutable($e);
$dotenv->load();
// Get the protocol (http or https)
$protocol = isset($_SERVER['REQUEST_SCHEME']) ? $_SERVER['REQUEST_SCHEME'] : 'http';
// Get the domain name
$domain = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost';
// Get the port number
$port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80';
// Combine for the SiteURL...
if ($port === "80" || $port === "443") {
define( "SITE_URL", "{$protocol}://{$domain}");
} else {
define( "SITE_URL", "{$protocol}://{$domain}:{$port}");
}
define( "DB_DSN", $_ENV['DB_TYPE'].":host=".$_ENV['DB_HOST'].";dbname=".$_ENV['DB_NAME'] );
define( "DB_USERNAME", $_ENV['DB_USERNAME'] );
define( "DB_PASSWORD", $_ENV['DB_PASSWORD']);
define( "CLASS_PATH", "../protected/src/classes" );
define( "TEMPLATE_PATH", "../protected/src/templates" );
$admins = $_ENV['ADMIN_USERS'];
$usersArray = explode(',', $admins);
$a_admins = [];
foreach ($usersArray as $user) {
list($username, $password) = explode(':', $user);
$a_admins[$username] = $password;
}
define( "ADMIN_USERS", $a_admins);
define( "ARTICLE_IMAGE_PATH", "../protected/images/articles" );
define( "IMG_TYPE_FULLSIZE", "fullsize" );
define( "IMG_TYPE_THUMB", "thumb" );
define( "ARTICLE_THUMB_WIDTH", 120 );
define( "JPEG_QUALITY", 85 );
require( CLASS_PATH . "/Article.php" );
require( CLASS_PATH . "/Category.php" );
function handleException( $exception ) {
echo "Sorry, a problem occurred. Please try later.";
// echo $exception->getMessage();
error_log( $exception->getMessage() );
}
set_exception_handler( 'handleException' );

@ -0,0 +1,28 @@
DROP TABLE IF EXISTS categories;
CREATE TABLE categories
(
id smallint unsigned NOT NULL auto_increment,
`name` varchar(255) NOT NULL, # Name of the category
description text NOT NULL, # A short description of the category
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS articles;
CREATE TABLE articles
(
id smallint unsigned NOT NULL auto_increment,
publicationDate date NOT NULL, # When the article was published
categoryId smallint unsigned NOT NULL, # The article category ID
title varchar(255) NOT NULL, # Full title of the article
summary text NOT NULL, # A short summary of the article
content mediumtext NOT NULL, # The HTML content of the article
imageExtension varchar(255) NOT NULL, # The filename extension of the article's full-size and thumbnail images
PRIMARY KEY (id)
);
# CREATE USER 'cms'@'localhost' IDENTIFIED BY 'SJ6G*WyaV7PvvEts@vxjm';
# GRANT ALL ON cms.* TO 'cms'@'localhost';
# create database cms;

@ -0,0 +1,197 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<?php include TEMPLATE_PATH. "/admin/include/header.php" ?>
<script type="text/javascript" src="assets/js/ckeditor.js"></script>
<script>
// Prevents file upload hangs in Mac Safari
// Inspired by http://airbladesoftware.com/notes/note-to-self-prevent-uploads-hanging-in-safari
function closeKeepAlive() {
if ( /AppleWebKit|MSIE/.test( navigator.userAgent) ) {
var xhr = new XMLHttpRequest();
xhr.open( "GET", "/ping/close", false );
xhr.send();
}
}
</script>
<h1><?php echo $results['pageTitle']?></h1>
<form action="admin.php?action=<?php echo $results['formAction']?>" method="post" enctype="multipart/form-data" onsubmit="closeKeepAlive()">
<input type="hidden" name="articleId" value="<?php echo $results['article']->id ?>"/>
<?php if ( isset( $results['errorMessage'] ) ) { ?>
<div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>
<ul>
<li>
<label for="title">Article Title</label>
<input type="text" name="title" id="title" placeholder="Name of the article" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $results['article']->title )?>" />
</li>
<li>
<label for="summary">Article Summary</label>
<textarea name="summary" id="summary" placeholder="Brief description of the article" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars( $results['article']->summary )?></textarea>
</li>
<li>
<label for="content">Article Content</label>
<textarea name="content" id="content" placeholder="The HTML content of the article" required maxlength="100000" style="height: 30em;"><?php echo htmlspecialchars( $results['article']->content ?? " ")?></textarea>
</li>
<?php
$catID = $results['article']->categoryId ?? 0;
?>
<li>
<label for="categoryId">Article Category</label>
<select name="categoryId" id="categoryId">
<option value="0"<?php echo ($catID > 0) ? " selected" : ""?>>(none)</option>
<?php foreach ( $results['categories'] as $category ) { ?>
<option value="<?php echo $category->id?>"<?php echo ( $category->id == $catID ) ? " selected" : ""?>><?php echo htmlspecialchars( $category->name )?></option>
<?php } ?>
</select>
</li>
<li>
<label for="publicationDate">Publication Date</label>
<input type="date" name="publicationDate" id="publicationDate" placeholder="YYYY-MM-DD" required maxlength="10" value="<?php echo $results['article']->publicationDate ? date( "Y-m-d", $results['article']->publicationDate ) : "" ?>" />
</li>
<?php if ( $results['article'] && $imagePath = $results['article']->getImagePath() ) { ?>
<li>
<label>Current Image</label>
<img id="articleImage" src="<?php echo $imagePath ?>" alt="Article Image" />
</li>
<li>
<label></label>
<input type="checkbox" name="deleteImage" id="deleteImage" value="yes"/ > <label for="deleteImage">Delete</label>
</li>
<?php } ?>
<li>
<label for="image">New Image</label>
<input type="file" name="image" id="image" placeholder="Choose an image to upload" maxlength="255" />
</li>
</ul>
<div class="buttons">
<input type="submit" name="saveChanges" value="Save Changes" />
<input type="submit" formnovalidate name="cancel" value="Cancel" />
</div>
</form>
<?php if ( $results['article']->id ) { ?>
<p><a href="admin.php?action=deleteArticle&amp;articleId=<?php echo $results['article']->id ?>" onclick="return confirm('Delete This Article?')">Delete This Article</a></p>
<?php } ?>
<script type="text/javascript">
class MyUploadAdapter {
constructor(loader) {
// The file loader instance to use during the upload.
this.loader = loader;
}
// Starts the upload process.
upload() {
return this.loader.file
.then(file => new Promise((resolve, reject) => {
this._initRequest();
this._initListeners(resolve, reject, file);
this._sendRequest(file);
}));
}
// Aborts the upload process.
abort() {
if (this.xhr) {
this.xhr.abort();
}
}
// Initializes the XMLHttpRequest object.
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
// Set the request URL.
xhr.open('POST', 'upload.php', true);
// Set up a progress listener to track the progress of the upload.
xhr.upload.addEventListener('progress', evt => {
if (evt.lengthComputable) {
this.loader.uploadTotal = evt.total;
this.loader.uploaded = evt.loaded;
}
});
}
// Initializes XMLHttpRequest listeners.
_initListeners(resolve, reject, file) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${file.name}.`;
xhr.addEventListener('error', () => reject(genericErrorText));
xhr.addEventListener('abort', () => reject());
xhr.addEventListener('load', () => {
try {
const response = JSON.parse(xhr.response);
// console.log(response);
// Check if the upload was successful.
if (!response || response.error) {
alert(response.error);
return reject(response && response.error ? response.error.message : genericErrorText);
}
//console.info("Success!!!");
//console.info(response.url);
// If the upload is successful, resolve the loader with the uploaded file's URL.
resolve({
default: response.url
});
} catch (error) {
console.error('An error occurred:', error);
}
});
if (xhr.upload) {
xhr.upload.addEventListener('progress', evt => {
if (evt.lengthComputable) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
});
}
}
// Sends the request to the server.
_sendRequest(file) {
const data = new FormData();
// Append any additional parameters needed by your server.
// data.append('param1', 'value1');
// Append the file to the form data.
data.append('upload', file);
// Send the request.
this.xhr.send(data);
}
}
ClassicEditor
.create( document.querySelector( '#content' ), {
// toolbar: ['imageUpload', 'heading', '|', 'bold', 'italic', 'link']
} )
.then( editor => {
editor.plugins.get('FileRepository').createUploadAdapter = loader => new MyUploadAdapter(loader);
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
</script>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,38 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<?php include TEMPLATE_PATH. "/admin/include/header.php" ?>
<h1><?php echo $results['pageTitle']?></h1>
<form action="admin.php?action=<?php echo $results['formAction']?>" method="post">
<input type="hidden" name="categoryId" value="<?php echo $results['category']->id ?>"/>
<?php if ( isset( $results['errorMessage'] ) ) { ?>
<div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>
<ul>
<li>
<label for="name">Category Name</label>
<input type="text" name="name" id="name" placeholder="Name of the category" required autofocus maxlength="255" value="<?php echo htmlspecialchars( $results['category']->name )?>" />
</li>
<li>
<label for="description">Description</label>
<textarea name="description" id="description" placeholder="Brief description of the category" required maxlength="1000" style="height: 5em;"><?php echo htmlspecialchars( $results['category']->description )?></textarea>
</li>
</ul>
<div class="buttons">
<input type="submit" name="saveChanges" value="Save Changes" />
<input type="submit" formnovalidate name="cancel" value="Cancel" />
</div>
</form>
<?php if ( $results['category']->id ) { ?>
<p><a href="admin.php?action=deleteCategory&amp;categoryId=<?php echo $results['category']->id ?>" onclick="return confirm('Delete This Category?')">Delete This Category</a></p>
<?php } ?>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,4 @@
<div id="adminHeader">
<h2><?= BLOG_NAME ?> Admin</h2>
<p>You are logged in as <b><?php echo htmlspecialchars( $_SESSION['username']) ?></b>. <a href="admin.php?action=listArticles">Edit Articles</a> <a href="admin.php?action=listCategories">Edit Categories</a> <a href="admin.php?action=logout"?>Log Out</a></p>
</div>

@ -0,0 +1,46 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<?php include TEMPLATE_PATH. "/admin/include/header.php" ?>
<h1>All Articles</h1>
<?php if ( isset( $results['errorMessage'] ) ) { ?>
<div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>
<?php if ( isset( $results['statusMessage'] ) ) { ?>
<div class="statusMessage"><?php echo $results['statusMessage'] ?></div>
<?php } ?>
<table>
<tr>
<th>Publication Date</th>
<th>Article</th>
<th>Category</th>
</tr>
<?php foreach ( $results['articles'] as $article ) { ?>
<tr onclick="location='admin.php?action=editArticle&amp;articleId=<?php echo $article->id?>'">
<td><?php echo date('j M Y', $article->publicationDate)?></td>
<td>
<?php echo $article->title?>
</td>
<td>
<?php
if ( isset( $results['categories'] ) && isset( $results['categories'][$article->categoryId] ) ) {
echo $results['categories'][$article->categoryId]?->name;
}
?>
</td>
</tr>
<?php } ?>
</table>
<p><?php echo $results['totalRows']?> article<?php echo ( $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p>
<p><a href="admin.php?action=newArticle">Add a New Article</a></p>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,36 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<?php include TEMPLATE_PATH. "/admin/include/header.php" ?>
<h1>Article Categories</h1>
<?php if ( isset( $results['errorMessage'] ) ) { ?>
<div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>
<?php if ( isset( $results['statusMessage'] ) ) { ?>
<div class="statusMessage"><?php echo $results['statusMessage'] ?></div>
<?php } ?>
<table>
<tr>
<th>Category</th>
</tr>
<?php foreach ( $results['categories'] as $category ) { ?>
<tr onclick="location='admin.php?action=editCategory&amp;categoryId=<?php echo $category->id?>'">
<td>
<?php echo $category->name?>
</td>
</tr>
<?php } ?>
</table>
<p><?php echo $results['totalRows']?> categor<?php echo ( $results['totalRows'] != 1 ) ? 'ies' : 'y' ?> in total.</p>
<p><a href="admin.php?action=newCategory">Add a New Category</a></p>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,30 @@
<?php include TEMPLATE_PATH."/include/header.php" ?>
<form action="admin.php?action=login" method="post" style="width: 50%;">
<input type="hidden" name="login" value="true" />
<?php if ( isset( $results['errorMessage'] ) ) { ?>
<div class="errorMessage"><?php echo $results['errorMessage'] ?></div>
<?php } ?>
<ul>
<li>
<label for="username">Username</label>
<input type="text" name="username" id="username" placeholder="Your admin username" required autofocus maxlength="20" />
</li>
<li>
<label for="password">Password</label>
<input type="password" name="password" id="password" placeholder="Your admin password" required maxlength="20" />
</li>
</ul>
<div class="buttons">
<input type="submit" name="login" value="Login" />
</div>
</form>
<?php include TEMPLATE_PATH."/include/footer.php";

@ -0,0 +1,35 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<h1><?php echo htmlspecialchars( $results['pageHeading'] ) ?></h1>
<?php if ( $results['category'] ) { ?>
<h3 class="categoryDescription"><?php echo htmlspecialchars( $results['category']->description ) ?></h3>
<?php } ?>
<ul id="headlines" class="archive">
<?php foreach ( $results['articles'] as $article ) { ?>
<li>
<h2>
<span class="pubDate"><?php echo date('j F Y', $article->publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
<?php if ( !$results['category'] && $article->categoryId ) { ?>
<span class="category">in <a href=".?action=archive&amp;categoryId=<?php echo $article->categoryId?>"><?php echo htmlspecialchars( $results['categories'][$article->categoryId]->name ) ?></a></span>
<?php } ?>
</h2>
<p class="summary">
<?php if ( $imagePath = $article->getImagePath( IMG_TYPE_THUMB ) ) { ?>
<a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><img class="articleImageThumb" src="<?php echo $imagePath?>" alt="Article Thumbnail" /></a>
<?php } ?>
<?php echo htmlspecialchars( $article->summary )?>
</p>
</li>
<?php } ?>
</ul>
<p><?php echo $results['totalRows']?> article<?php echo ( $results['totalRows'] != 1 ) ? 's' : '' ?> in total.</p>
<p><a href="./">Return to Homepage</a></p>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,34 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<ul id="headlines">
<?php
$c = 0;
foreach ( $results['articles'] as $article ) { ?>
<li>
<h2>
<span class="pubDate"><?php echo date('j M Y', $article->publicationDate)?></span><a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><?php echo htmlspecialchars( $article->title )?></a>
<?php if ( $article->categoryId ) { ?>
<span class="category">in <a href=".?action=archive&amp;categoryId=<?php echo $article->categoryId?>"><?php echo htmlspecialchars( $results['categories'][$article->categoryId]->name )?></a></span>
<?php } ?>
</h2>
<p class="summary">
<?php if ( $imagePath = $article->getImagePath( IMG_TYPE_THUMB ) ) { ?>
<a href=".?action=viewArticle&amp;articleId=<?php echo $article->id?>"><img class="articleImageThumb" src="<?php echo $imagePath?>" alt="Article Thumbnail" /></a>
<?php } ?>
<?php echo htmlspecialchars( $article->summary )?>
</p>
</li>
<?php
$c++;
} ?>
</ul>
<?php if ($c < $results['totalRows']) { ?>
<p><a href="./?action=archive">Article Archive</a></p>
<?php } ?>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,7 @@
<div id="footer">
<?= BLOG_NAME ?> &copy; <?= date('Y') ?>. All rights reserved. <a href="admin.php">Site Admin</a>
</div>
</div>
</body>
</html>

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title><?php echo htmlspecialchars( $results['pageTitle'] )?></title>
<link rel="stylesheet" type="text/css" href="assets/css/style.css" />
</head>
<body>
<div id="container">
<a href="."><img id="logo" src="assets/images/logo.jpg" alt="Logo for Blog post" /></a>
<div id="logo-hr"></div>

@ -0,0 +1,19 @@
<?php include TEMPLATE_PATH. "/include/header.php" ?>
<h1 style="width: 75%;"><?php echo htmlspecialchars( $results['article']->title )?></h1>
<div style="width: 75%; font-style: italic;"><?php echo htmlspecialchars( $results['article']->summary ) ?></div>
<div style="width: 75%; min-height: 300px;">
<?php if ( $imagePath = $results['article']->getImagePath() ) { ?>
<img id="articleImageFullsize" src="<?php echo $imagePath ?>" alt="Article Image" />
<?php } ?>
<?php echo $results['article']->content ?>
</div>
<p class="pubDate">Published on <?php echo date('j F Y', $results['article']->publicationDate)?>
<?php if ( $results['category'] ) { ?>
in <a href="./?action=archive&amp;categoryId=<?php echo $results['category']->id?>"><?php echo htmlspecialchars( $results['category']->name ) ?></a>
<?php } ?>
</p>
<p><a href="./">Return to Homepage</a></p>
<?php include TEMPLATE_PATH. "/include/footer.php";

@ -0,0 +1,304 @@
<?php
require "../protected/src/config.php";
session_start();
$action = isset( $_GET['action'] ) ? $_GET['action'] : "";
$username = isset( $_SESSION['username'] ) ? $_SESSION['username'] : "";
if ( $action != "login" && $action != "logout" && !$username ) {
login();
exit;
}
switch ( $action ) {
case 'login':
login();
break;
case 'logout':
logout();
break;
case 'newArticle':
newArticle();
break;
case 'editArticle':
editArticle();
break;
case 'deleteArticle':
deleteArticle();
break;
case 'listCategories':
listCategories();
break;
case 'newCategory':
newCategory();
break;
case 'editCategory':
editCategory();
break;
case 'deleteCategory':
deleteCategory();
break;
default:
listArticles();
}
// Function to check if a user and password match
function isValidUser($username, $password) {
// Check if the username exists in the ADMIN_USERS array
if (array_key_exists($username, ADMIN_USERS)) {
// Check if the provided password matches the stored password for the username
if (ADMIN_USERS[$username] === $password) {
return true; // Username and password are valid
}
}
return false; // Username and/or password are invalid
}
function invalidUser() {
// Login failed: display an error message to the user
$results['errorMessage'] = "Incorrect username or password. Please try again.";
require( TEMPLATE_PATH . "/admin/loginForm.php" );
exit;
}
function login() {
$results = array();
$results['pageTitle'] = "Admin Login | Widget News";
if ( isset( $_POST['login'] ) ) {
$usr = $_POST['username'] ?? false;
$pwd = $_POST['password'] ?? false;
if ($usr === false || $pwd === false) {
invalidUser();
}
// User has posted the login form: attempt to log the user in
if (isValidUser($usr, $pwd)) {
// Login successful: Create a session and redirect to the admin homepage
$_SESSION['username'] = $usr;
header( "Location: admin.php" );
} else {
invalidUser();
}
} else {
// User has not posted the login form yet: display the form
require( TEMPLATE_PATH . "/admin/loginForm.php" );
}
}
function logout() {
unset( $_SESSION['username'] );
header( "Location: admin.php" );
}
function newArticle() {
$results = array();
$results['pageTitle'] = "New Article";
$results['formAction'] = "newArticle";
if ( isset( $_POST['saveChanges'] ) ) {
// User has posted the article edit form: save the new article
$article = new Article;
$article->storeFormValues( $_POST );
$article->insert();
if ( isset( $_FILES['image'] ) ) $article->storeUploadedImage( $_FILES['image'] );
header( "Location: admin.php?status=changesSaved" );
} elseif ( isset( $_POST['cancel'] ) ) {
// User has cancelled their edits: return to the article list
header( "Location: admin.php" );
} else {
// User has not posted the article edit form yet: display the form
$results['article'] = new Article;
$data = Category::getList();
$results['categories'] = $data['results'];
require( TEMPLATE_PATH . "/admin/editArticle.php" );
}
}
function editArticle() {
$results = array();
$results['pageTitle'] = "Edit Article";
$results['formAction'] = "editArticle";
if ( isset( $_POST['saveChanges'] ) ) {
// User has posted the article edit form: save the article changes
if ( !$article = Article::getById( (int)$_POST['articleId'] ) ) {
header( "Location: admin.php?error=articleNotFound" );
return;
}
$article->storeFormValues( $_POST );
if ( isset($_POST['deleteImage']) && $_POST['deleteImage'] == "yes" ) $article->deleteImages();
$article->update();
if ( isset( $_FILES['image'] ) ) $article->storeUploadedImage( $_FILES['image'] );
header( "Location: admin.php?status=changesSaved" );
} elseif ( isset( $_POST['cancel'] ) ) {
// User has cancelled their edits: return to the article list
header( "Location: admin.php" );
} else {
// User has not posted the article edit form yet: display the form
$results['article'] = Article::getById( (int)$_GET['articleId'] );
$data = Category::getList();
$results['categories'] = $data['results'];
require( TEMPLATE_PATH . "/admin/editArticle.php" );
}
}
function deleteArticle() {
if ( !$article = Article::getById( (int)$_GET['articleId'] ) ) {
header( "Location: admin.php?error=articleNotFound" );
return;
}
$article->deleteImages();
$article->delete();
header( "Location: admin.php?status=articleDeleted" );
}
function listArticles() {
$results = array();
$data = Article::getList();
$results['articles'] = $data['results'];
$results['totalRows'] = $data['totalRows'];
$data = Category::getList();
$results['categories'] = array();
foreach ( $data['results'] as $category ) $results['categories'][$category->id] = $category;
$results['pageTitle'] = "All Articles";
if ( isset( $_GET['error'] ) ) {
if ( $_GET['error'] == "articleNotFound" ) $results['errorMessage'] = "Error: Article not found.";
}
if ( isset( $_GET['status'] ) ) {
if ( $_GET['status'] == "changesSaved" ) $results['statusMessage'] = "Your changes have been saved.";
if ( $_GET['status'] == "articleDeleted" ) $results['statusMessage'] = "Article deleted.";
}
require( TEMPLATE_PATH . "/admin/listArticles.php" );
}
function listCategories() {
$results = array();
$data = Category::getList();
$results['categories'] = $data['results'];
$results['totalRows'] = $data['totalRows'];
$results['pageTitle'] = "Article Categories";
if ( isset( $_GET['error'] ) ) {
if ( $_GET['error'] == "categoryNotFound" ) $results['errorMessage'] = "Error: Category not found.";
if ( $_GET['error'] == "categoryContainsArticles" ) $results['errorMessage'] = "Error: Category contains articles. Delete the articles, or assign them to another category, before deleting this category.";
}
if ( isset( $_GET['status'] ) ) {
if ( $_GET['status'] == "changesSaved" ) $results['statusMessage'] = "Your changes have been saved.";
if ( $_GET['status'] == "categoryDeleted" ) $results['statusMessage'] = "Category deleted.";
}
require( TEMPLATE_PATH . "/admin/listCategories.php" );
}
function newCategory() {
$results = array();
$results['pageTitle'] = "New Article Category";
$results['formAction'] = "newCategory";
if ( isset( $_POST['saveChanges'] ) ) {
// User has posted the category edit form: save the new category
$category = new Category;
$category->storeFormValues( $_POST );
$category->insert();
header( "Location: admin.php?action=listCategories&status=changesSaved" );
} elseif ( isset( $_POST['cancel'] ) ) {
// User has cancelled their edits: return to the category list
header( "Location: admin.php?action=listCategories" );
} else {
// User has not posted the category edit form yet: display the form
$results['category'] = new Category;
require( TEMPLATE_PATH . "/admin/editCategory.php" );
}
}
function editCategory() {
$results = array();
$results['pageTitle'] = "Edit Article Category";
$results['formAction'] = "editCategory";
if ( isset( $_POST['saveChanges'] ) ) {
// User has posted the category edit form: save the category changes
if ( !$category = Category::getById( (int)$_POST['categoryId'] ) ) {
header( "Location: admin.php?action=listCategories&error=categoryNotFound" );
return;
}
$category->storeFormValues( $_POST );
$category->update();
header( "Location: admin.php?action=listCategories&status=changesSaved" );
} elseif ( isset( $_POST['cancel'] ) ) {
// User has cancelled their edits: return to the category list
header( "Location: admin.php?action=listCategories" );
} else {
// User has not posted the category edit form yet: display the form
$results['category'] = Category::getById( (int)$_GET['categoryId'] );
require( TEMPLATE_PATH . "/admin/editCategory.php" );
}
}
function deleteCategory() {
if ( !$category = Category::getById( (int)$_GET['categoryId'] ) ) {
header( "Location: admin.php?action=listCategories&error=categoryNotFound" );
return;
}
$articles = Article::getList( 1000000, $category->id );
if ( $articles['totalRows'] > 0 ) {
header( "Location: admin.php?action=listCategories&error=categoryContainsArticles" );
return;
}
$category->delete();
header( "Location: admin.php?action=listCategories&status=categoryDeleted" );
}

@ -0,0 +1,391 @@
/* Style the body and outer container */
:root {
--background: #051622;
--container: #ffffff;
--h1: red;
--h2: darkblue;
--pubDate: red;
--containerWidth: 75vw;
}
body {
margin: 0;
color: #333;
background-color: var(--background);
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
line-height: 1.5em;
}
#container {
width: var(--containerWidth);
background: var(--container);
margin: 20px auto;
padding: 20px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
}
/* The logo and footer */
#logo {
display: block;
/* width: 300px; */
border: none;
margin-bottom: 40px;
/* make Round Image */
border-radius:50%;
overflow:hidden;
}
#logo-hr {
border-top: 1px solid var(--background);
margin-top: 40px;
padding: 20px 0 0 0;
font-size: .8em;
}
#footer {
border-top: 1px solid var(--background);
margin-top: 40px;
padding: 20px 0 0 0;
font-size: .8em;
}
/* Headings */
h1 {
color: var(--h1);
margin-bottom: 30px;
line-height: 1.2em;
}
h2, h2 a {
color: var(--h2);
}
h2 a {
text-decoration: none;
}
h3.categoryDescription {
margin-top: -20px;
margin-bottom: 40px;
}
/* Article headlines */
#headlines {
list-style: none;
padding-left: 0;
width: 75%;
}
#headlines li {
margin-bottom: 2em;
clear: both;
}
.pubDate {
font-size: .8em;
color: var(--pubDate);
text-transform: uppercase;
}
#headlines .pubDate {
display: block;
width: 100px;
padding-top: 4px;
float: left;
font-size: .5em;
vertical-align: middle;
}
#headlines.archive .pubDate {
width: 130px;
}
#headlines .articleImageThumb {
width: 120px;
float: left;
margin: 4px 20px 0 0;
border: 1px solid #00a0b0;
}
.summary {
padding-left: 100px;
}
#headlines.archive .summary {
padding-left: 130px;
}
/* Article pages */
#articleImageFullsize {
width: 250px;
float: left;
margin: 4px 20px 10px 0;
border: 1px solid #00a0b0;
}
.category {
font-style: italic;
font-weight: normal;
font-size: 60%;
color: #999;
display: block;
line-height: 2em;
}
.category a {
color: #999;
text-decoration: underline;
}
/* "You are logged in..." header on admin pages */
#adminHeader {
width: 940px;
padding: 0 10px;
border-bottom: 1px solid #00a0b0;
margin: -30px 0 40px 0;
font-size: 0.8em;
}
/* Style the form with a coloured background, along with curved corners and a drop shadow */
form {
margin: 20px auto;
padding: 40px 20px;
overflow: auto;
background: #fff4cf;
border: 1px solid #666;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
-webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
}
/* Give form elements consistent margin, padding and line height */
form ul {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
}
form ul li {
margin: .9em 0 0 0;
padding: 0;
}
form * {
line-height: 1em;
}
/* The field labels */
label {
display: block;
float: left;
clear: left;
text-align: right;
width: 15%;
padding: .4em 0 0 0;
margin: .15em .5em 0 0;
}
/* The fields */
input, select, textarea {
display: block;
margin: 0;
padding: .4em;
width: 80%;
}
input, textarea, .date {
border: 2px solid #666;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
background: #fff;
}
input {
font-size: .9em;
}
input[type="checkbox"] {
display: inline-block;
padding: 0;
margin: 0 0 .8em 0;
width: auto;
}
select {
padding: 0;
margin-bottom: 2.5em;
position: relative;
top: .7em;
}
textarea {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: .9em;
height: 5em;
line-height: 1.5em;
}
textarea#content {
font-family: "Courier New", courier, fixed;
}
#articleImage {
border: 2px solid #666;
}
#deleteImage {
clear: both;
}
label[for="deleteImage"] {
float: none;
display: inline;
}
input[type="file"] {
float: left;
}
/* Place a border around focused fields */
form *:focus {
border: 2px solid #7c412b;
outline: none;
}
/* Display correctly filled-in fields with a green background */
input:valid, textarea:valid {
background: #efe;
}
/* Submit buttons */
.buttons {
text-align: center;
margin: 40px 0 0 0;
}
input[type="submit"] {
display: inline;
margin: 0 20px;
width: 12em;
padding: 10px;
border: 2px solid #7c412b;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
-webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
color: #fff;
background: #ef7d50;
font-weight: bold;
-webkit-appearance: none;
}
input[type="submit"]:hover, input[type="submit"]:active {
cursor: pointer;
background: #fff;
color: #ef7d50;
}
input[type="submit"]:active {
background: #eee;
-moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
-webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
box-shadow: 0 0 .5em rgba(0, 0, 0, .8) inset;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
}
tr, th, td {
padding: 10px;
margin: 0;
text-align: left;
}
table, th {
border: 1px solid #00a0b0;
}
th {
border-left: none;
border-right: none;
background: #ef7d50;
color: #fff;
cursor: default;
}
tr:nth-child(odd) {
background: #fff4cf;
}
tr:nth-child(even) {
background: #fff;
}
tr:hover {
background: #ddd;
cursor: pointer;
}
/* Status and error boxes */
.statusMessage, .errorMessage {
font-size: .8em;
padding: .5em;
margin: 2em 0;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
-webkit-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
-box-shadow: 0 0 .5em rgba(0, 0, 0, .8);
}
.statusMessage {
background-color: #2b2;
border: 1px solid #080;
color: #fff;
}
.errorMessage {
background-color: #f22;
border: 1px solid #800;
color: #fff;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@ -0,0 +1,39 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { ClassicEditor as ClassicEditorBase } from '@ckeditor/ckeditor5-editor-classic';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { UploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder';
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
import { CKBox } from '@ckeditor/ckeditor5-ckbox';
import { CKFinder } from '@ckeditor/ckeditor5-ckfinder';
import { EasyImage } from '@ckeditor/ckeditor5-easy-image';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { Image, ImageCaption, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image';
import { Indent } from '@ckeditor/ckeditor5-indent';
import { Link } from '@ckeditor/ckeditor5-link';
import { List } from '@ckeditor/ckeditor5-list';
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
export default class ClassicEditor extends ClassicEditorBase {
static builtinPlugins: (typeof TextTransformation | typeof Essentials | typeof UploadAdapter | typeof Paragraph | typeof Heading | typeof Autoformat | typeof Bold | typeof Italic | typeof BlockQuote | typeof Image | typeof ImageCaption | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof CloudServices | typeof CKBox | typeof CKFinder | typeof EasyImage | typeof List | typeof Indent | typeof Link | typeof MediaEmbed | typeof PasteFromOffice | typeof Table | typeof TableToolbar | typeof PictureEditing)[];
static defaultConfig: {
toolbar: {
items: string[];
};
image: {
toolbar: string[];
};
table: {
contentToolbar: string[];
};
language: string;
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,45 @@
<?php
require "../protected/src/config.php";
// Assuming private_images is a directory outside public_html
$privateImagePath = ARTICLE_IMAGE_PATH . "/";
// Get the image filename from the query string
if (isset($_GET['image'])) {
$imageFilename = basename($_GET['image']);
$pth = $_GET['rel'] ?? false;
$relPath = match($pth) {
'fullsize'=>"fullsize/",
'thumb'=>'thumb/',
default=>"",
};
$imagePath = $privateImagePath . $relPath . $imageFilename;
// Check if the file exists
if (file_exists($imagePath)) {
// Get the file extension
$fileExtension = pathinfo($imagePath, PATHINFO_EXTENSION);
// Set the appropriate Content-Type header based on the file extension
$header = match($fileExtension) {
'jpg'=>'Content-Type: image/jpeg',
'jpeg'=>'Content-Type: image/jpeg',
'gif'=>'Content-Type: image/gif',
'png'=>'Content-Type: image/png',
'webp'=>'Content-Type: image/webp',
default=>'HTTP/1.0 404 Not Found',
};
header($header);
if (str_contains($header, "Content-Type:")) {
// Read and output the image content
readfile($imagePath);
}
exit();
} else {
// Image not found, return a 404 error
header('HTTP/1.0 404 Not Found');
exit();
}
}

@ -0,0 +1,56 @@
<?php
require "../protected/src/config.php";
$action = isset( $_GET['action'] ) ? $_GET['action'] : "";
switch ( $action ) {
case 'archive':
archive();
break;
case 'viewArticle':
viewArticle();
break;
default:
homepage();
}
function archive() {
$results = array();
$categoryId = ( isset( $_GET['categoryId'] ) && $_GET['categoryId'] ) ? (int)$_GET['categoryId'] : null;
$results['category'] = Category::getById( $categoryId );
$data = Article::getList( 100000, $results['category'] ? $results['category']->id : null );
$results['articles'] = $data['results'];
$results['totalRows'] = $data['totalRows'];
$data = Category::getList();
$results['categories'] = array();
foreach ( $data['results'] as $category ) $results['categories'][$category->id] = $category;
$results['pageHeading'] = $results['category'] ? $results['category']->name : "Article Archive";
$results['pageTitle'] = $results['pageHeading'] . " | " . BLOG_NAME;
require( TEMPLATE_PATH . "/archive.php" );
}
function viewArticle() {
if ( !isset($_GET["articleId"]) || !$_GET["articleId"] ) {
homepage();
return;
}
$results = array();
$results['article'] = Article::getById( (int)$_GET["articleId"] );
$results['category'] = Category::getById( $results['article']->categoryId );
$results['pageTitle'] = $results['article']->title . " | " . BLOG_NAME;
require( TEMPLATE_PATH . "/viewArticle.php" );
}
function homepage() {
$results = array();
$data = Article::getList( HOMEPAGE_NUM_ARTICLES );
$results['articles'] = $data['results'];
$results['totalRows'] = $data['totalRows'];
$data = Category::getList();
$results['categories'] = array();
foreach ( $data['results'] as $category ) $results['categories'][$category->id] = $category;
$results['pageTitle'] = BLOG_NAME;
require( TEMPLATE_PATH . "/homepage.php" );
}

@ -0,0 +1,78 @@
<?php
require "../protected/src/config.php";
// Set the Content-Type header to indicate that the response is in JSON format
header('Content-Type: application/json');
// Allow credentials (if needed)
header('Access-Control-Allow-Credentials: true');
// Optionally, set additional headers (e.g., CORS headers), Set the allowed origin
if (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN'] == SITE_URL) {
header('Access-Control-Allow-Origin: ' . SITE_URL);
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Set the maximum file size to 10MB (in bytes)
$maxFileSize = 10 * 1024 * 1024; // 10MB
// Define valid image types
$validImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
// Check if the file is uploaded successfully
if (isset($_FILES['upload']) && $_FILES['upload']['error'] === UPLOAD_ERR_OK) {
$uploadDir = ARTICLE_IMAGE_PATH . "/"; // Set your upload directory
$tempFile = $_FILES['upload']['tmp_name'];
// Check file size
if ($_FILES['upload']['size'] >= $maxFileSize) {
echo json_encode(['error' => 'Uploaded file too big! Limit of 10MB']);
unlink($tempFile);
exit;
}
// Check file type
if (! in_array($_FILES['upload']['type'], $validImageTypes)) {
echo json_encode(['error' => 'Uploaded file not allowed image type (JPG, PNG, GIF)!']);
unlink($tempFile);
exit;
}
// Check if the temporary file contains PHP tags
$fileContent = file_get_contents($tempFile);
if (strpos($fileContent, '<?php') !== false) {
// PHP tags detected, do not move the file and send an error response
echo json_encode(['error' => 'Danger: Uploaded file contains PHP start tags!']);
unlink($tempFile);
exit;
}
// No PHP tags detected, move the uploaded file to the specified directory
if (UPLOAD_RND_FILE_NAMES) {
// Generate a unique filename to prevent overwriting
$fn = basename(uniqid('image_') . '.' . pathinfo($_FILES['upload']['name'], PATHINFO_EXTENSION));
$uploadedFile = $uploadDir . $fn;
} else {
$fn = basename($_FILES['upload']['name']);
$uploadedFile = $uploadDir . $fn;
if (file_exists($uploadedFile)) {
echo json_encode(['error' => 'Failed to uploaded file: Filename already exists!']);
exit;
}
}
if (move_uploaded_file($tempFile, $uploadedFile)) {
// File moved successfully, send the file URL
$fileUrl = SITE_URL . '/image.php?image=' . $fn;
echo json_encode(['url' => $fileUrl]);
} else {
// Failed to move the uploaded file, send an error response
echo json_encode(['error' => 'Failed to move the uploaded file.']);
}
} else {
// File upload failed, send an error response
echo json_encode(['error' => 'File upload failed.']);
}
Loading…
Cancel
Save