├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── api-issue.md │ ├── dashboard-issue.md │ ├── llm-issue.md │ ├── monitor-issue.md │ ├── project-managment-issue.md │ └── scan-issue.md ├── .gitignore ├── ACCESSIBILITY.md ├── CONTRIBUTE.md ├── LICENSE ├── README.md ├── actions ├── auth_callback.php ├── create_report.php ├── delete_property.php ├── delete_report.php ├── delete_report_filter_cookie.php ├── install.php ├── login.php ├── logout.php ├── process_page.php ├── process_property.php ├── process_scans.php ├── queue_report_filter_change.php ├── save_property_settings.php ├── save_report_filter_change.php └── save_report_title.php ├── api ├── index.php └── requests │ ├── chart.php │ ├── messages.php │ ├── occurrences.php │ ├── pages.php │ ├── properties.php │ ├── queued_scans.php │ ├── statuses.php │ └── tags.php ├── components ├── active_class.php ├── active_filters.php ├── chart.php ├── message_list.php ├── message_occurrences_list.php ├── page_list.php ├── page_occurrences_list.php ├── report_filter_search.php ├── report_header.php ├── success_or_error_message.php └── tag_list.php ├── composer.json ├── composer.lock ├── helpers ├── get_content.php ├── get_next_scannable_property.php ├── get_page.php ├── get_page_title.php ├── get_properties.php ├── get_property.php ├── get_report_filters.php ├── get_reports.php ├── get_scans.php ├── get_scans_count.php ├── get_title.php └── is_page_scanning.php ├── index.php ├── init.php ├── logo.svg ├── theme.css └── views ├── account.php ├── message.php ├── page.php ├── property_settings.php ├── report.php ├── report_settings.php ├── reports.php ├── scans.php ├── settings.php └── tag.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: bbertucc 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/api-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: API Issue 3 | about: This issue is related to Equalify's API. 4 | title: '' 5 | labels: API 6 | assignees: heythisischris 7 | 8 | --- 9 | 10 | ## Problem 11 | Describe the problem being addressed by the issue. 12 | 13 | ## Steps to Close This Issue 14 | - [ ] @bbertucc reviews issue with the assignee. 15 | - [ ] Assinee budgets issues. 16 | - [ ] Budget is approved/disapproved with comment. 17 | - [ ] Work commences 18 | - [ ] @bbertucc closes this issue after validating fix logging payment. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/dashboard-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dashboard Issue 3 | about: This issue is related to Equalify's dashboard. 4 | title: '' 5 | labels: '' 6 | assignees: wilsuriel03 7 | 8 | --- 9 | 10 | ## Problem 11 | Describe the problem being addressed by the issue. 12 | 13 | ## Steps to Close This Issue 14 | - [ ] @bbertucc reviews issue with the assignee. 15 | - [ ] Assinee budgets issues. 16 | - [ ] Budget is approved/disapproved with comment. 17 | - [ ] Work commences 18 | - [ ] @bbertucc closes this issue after validating fix logging payment. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/llm-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: LLM Issue 3 | about: This issue is related to Equalify's LLM 4 | title: '' 5 | labels: llm 6 | assignees: heythisischris 7 | 8 | --- 9 | 10 | ## Problem 11 | Describe the problem being addressed by the issue. 12 | 13 | ## Steps to Close This Issue 14 | - [ ] @bbertucc reviews issue with the assignee. 15 | - [ ] Assinee budgets issues. 16 | - [ ] Budget is approved/disapproved with comment. 17 | - [ ] Work commences 18 | - [ ] @bbertucc closes this issue after validating fix logging payment. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/monitor-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Monitor Issue 3 | about: This issue is related to Equalify's monitoring service. 4 | title: '' 5 | labels: '' 6 | assignees: azdak 7 | 8 | --- 9 | 10 | ## Problem 11 | Describe the problem being addressed by the issue. 12 | 13 | ## Steps to Close This Issue 14 | - [ ] @bbertucc reviews issue with the assignee. 15 | - [ ] Assinee budgets issues. 16 | - [ ] Budget is approved/disapproved with comment. 17 | - [ ] Work commences 18 | - [ ] @bbertucc closes this issue after validating fix logging payment. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/project-managment-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Project Managment Issue 3 | about: Related to Equalify Project Management 4 | title: '' 5 | labels: management 6 | assignees: bbertucc 7 | 8 | --- 9 | 10 | ## Problem 11 | Describe the problem being addressed by the issue. 12 | 13 | ## Steps to Close This Issue 14 | - [ ] @bbertucc reviews issue with the assignee. 15 | - [ ] Assinee budgets issues. 16 | - [ ] Budget is approved/disapproved with comment. 17 | - [ ] Work commences 18 | - [ ] @bbertucc closes this issue after validating fix logging payment. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/scan-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Scan Issue 3 | about: Issues related to Equalify's scan. 4 | title: '' 5 | labels: scan 6 | assignees: azdak 7 | 8 | --- 9 | 10 | ## Problem 11 | Describe the problem being addressed by the issue. 12 | 13 | ## Steps to Close This Issue 14 | - [ ] @bbertucc reviews issue with the assignee. 15 | - [ ] Assinee budgets issues. 16 | - [ ] Budget is approved/disapproved with comment. 17 | - [ ] Work commences 18 | - [ ] @bbertucc closes this issue after validating fix logging payment. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.ddev 2 | vendor/ 3 | *.log 4 | .github 5 | .vscode 6 | .DS_Store 7 | .vscode 8 | /assets 9 | login/ 10 | config-server.php 11 | test/ 12 | _dev/ 13 | _private/ 14 | .ddev/ 15 | test.json 16 | test.php 17 | test.html 18 | node_modules 19 | package.json 20 | package-lock.json 21 | .phpunit.result.cache 22 | .env-** 23 | .env 24 | cron.log 25 | composer.lock -------------------------------------------------------------------------------- /ACCESSIBILITY.md: -------------------------------------------------------------------------------- 1 | # Equalify Accessibility Statement 2 | 3 | ## How Can We Help? 4 | We welcome any comments, questions, or feedback on our site. If you notice aspects of our site that aren’t working for you or your assistive technology, please [submit an issue on Equalify's Github repo](https://github.com/EqualifyEverything/equalify/issues/new). 5 | 6 | ## Equalify is Committed to Digital Accessibility 7 | This project is committed to delivering an excellent user experience for everyone. Equalify's user interface is structured in a way that allows those of all abilities to easily and quickly find the information they need. 8 | 9 | ## Ongoing Efforts to Ensure Accessible Content 10 | Equalify uses the Web Content Accessibility Guidelines (WCAG) version 2.2 as its guiding principle. As we develop new pages and functionality, the principles of accessible design and development are an integral part of conception and realization. 11 | 12 | We continually test content and features for WCAG 2.2 Level AA compliance and remediate any issues to ensure we meet or exceed the standards. Testing of our digital content is performed by our accessibility experts using automated testing software, screen readers, a color contrast analyzer, and keyboard-only navigation techniques. 13 | 14 | ## Summary of Accessibility Features 15 | - All images and other non-text elements have alternative text associated with them. 16 | - Navigational aids are provided on all app pages. 17 | - Structural markup to indicate headings and lists has been provided to aid in page comprehension 18 | - Forms are associated with labels and instructions on filling in forms are available to screen reader users 19 | 20 | ## Project VPATs 21 | To request a conformance report using the Voluntary Product Accessibility Template (VPAT), please [submit an issue on Equalify's Github repo](https://github.com/EqualifyEverything/equalify/issues/new). -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Equalify code is first and foremost inclusive. We invite everyone, no matter their experience, to contribute to Equalify. 3 | 4 | This document outlines how we maintain an inclusive code base, open to all contributors. 5 | 6 | ## Key Guidelines 7 | These guidelines are used to assess new code: 8 | 9 | 1. **Work Procedurally**: Procedural programming works with the basic Accessibility premise of well-ordered content (for more, check out [Mozilla’s discussion on “Proper Semantics”](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#good_semantics)). We know that’s different than many software projects that value Object-oriented programming, but we have enjoyed the fact that many new-to-PHP users understand our code easily. 10 | 2. **Name Clearly**: Lots of code comments often mean that functions and variables are not named. We value explicit naming of functions and variables instead of adding lots of comments about what the functions or variables do. 11 | 3.** Write in PHP**: PHP became our programming language of choice after building early prototypes in Python and JavaScript. We didn’t choose Python because it wasn’t familiar to many website developers we started working with. We didn’t choose JavaScript because promoted working in a way that worked against screen reader users. We always remain open to change if that means making our platform’s code more accessible to users. 12 | 4. **Avoid Frameworks**: New frameworks must save us time without adding new barriers for contributors. Under that creed, we find ourselves going back to coding solutions in basic PHP instead of adopting frameworks. 13 | 5. **Be Efficient, But Not At Expense of Clarity. **Remember: we are an accessibility platform. We want to be fast and agile. That said, we’ll gladly trade a small efficiency to be more clear to our contributors. We think the smartest solutions are both super efficient and super understandable. 14 | 6. **Act Without Dogma**: Of course, all of our ideas are up for debate! Please create a pull request to let us know of any new ideas. We’re excited to evolve into the most inclusive platform to have ever shaped the internet. 15 | 16 | ## Ready to get started? 17 | Here are some steps to start contributing: 18 | 19 | 1. Add a new ticket to [issues](https://github.com/EqualifyEverything/equalify/issues) or create a pull request for changes. 20 | 2. @bbertucc is the principal maintainer, and he'll chime in on any new issues or PR with the next steps. 21 | 3. Followup! Feel free to follow up on any issue or PR. Some things slip through the cracks. 22 | 4. Usually a discussion ensues before some clear tasks are assigned or a PR is approved. 23 | 5. Approved PRs and work will be tested before entering the `main` branch and turned into a release. 24 | 25 | ## Questions? 26 | Open up a new issue with any question. Maintainers or the community will answer. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Equalify Logo 2 | 3 | ## Better Accessibility Management 4 | Equalify aims to be the most useful accessibility platform. That means faster scanning, more accurate results, and a more intuitive user interface. We publish Equalify code here so that you can run the platform locally, building new features and fixing issues. 5 | 6 | ## Managed Service 7 | Not technical? Want to support Equalify? 8 | 9 | Visit [https://equalify.app](https://equalify.app) to try our hosted service. 10 | 11 | The service is fully supported and super fast. Plus, you'll get these features: 12 | - Automatic Scans 13 | - Scheduled Scans 14 | - Multi-User Administration 15 | - Shareable Reports 16 | 17 | And please star this repo! 18 | 19 | Your support sustains open source work. 20 | 21 | ## Setup 22 | After forking the repo: 23 | 1. Create `.env` with the following: 24 | ``` 25 | ## DB Info 26 | DB_HOST= 27 | DB_USERNAME= 28 | DB_PASSWORD= 29 | DB_NAME= 30 | DB_PORT= 31 | 32 | ## Scan Info 33 | SCAN_URL= 34 | ``` 35 | 2. Run `composer install` to integrate upload script(you may need to install composer first) 36 | 3. Run in your favorite local LAMP/LEMP setup. (We love [ddev](https://github.com/ddev/ddev)!) 37 | 4. Run `php actions/install.php` to create the tables. 38 | 5. Equalify everything! 39 | 40 | PHP 8.1+ is required with MySQL 8.0+. 41 | 42 | ## Contribute 43 | Submit bug reports, questions, and patches to the repo's [issues](https://github.com/EqualifyEverything/equalify/issues) tab. 44 | 45 | If you would like to submit a pull request, please read [CONTRIBUTE.md](/CONTRIBUTE.md) and [ACCESSIBILITY.md](/ACCESSIBILITY.md) before you do. 46 | 47 | ## Special Thanks 48 | A chaos wizard 🧙, [Bruno Lowagie](https://lowagie.com), and many others help Equalify. The project is run by [@bbertucc](https://github.com/bbertucc). Special shout out to [Pantheon](https://pantheon.io/) and [Little Forest](https://littleforest.co.uk/feature/web-accessibility/) for providing funding for [bounties](https://github.com/bbertucc/equalify/issues?q=is%3Aopen+is%3Aissue+label%3Abountied). Yi, Kate, Bill, Dash, Sylvia, Anne, Doug, Matt, Nathan, and John- You are the brains that helped launch this idea. [@ebertucc](https://github.com/ebertucc) and [@jrchamp](https://github.com/jrchamp) are the project's first contributors - woot woot! Much help also came from [mgifford](https://github.com/mgifford), [kreynen](https://github.com/kreynen), and [j-mendez](https://github.com/j-mendez) - you all rock! [Guzzle](https://github.com/guzzle/guzzle) makes multiple concurrent scans possible. [Composer](https://getcomposer.org/) makes Guzzle possible. [TolstoyDotCom](https://github.com/TolstoyDotCom) and [zersiax](https://github.com/zersiax) were our first hired contributors. [azsak](https://github.com/azdak) currently keeps the scan chugging. And of course shoutout to [Decubing](https://github.com/decubing) - they built our MVP! 49 | 50 | This project's code is published under the [GNU Affero General Public License v3.0](https://github.com/bbertucc/equalify/blob/main/LICENSE) to inspire new collaborations. 51 | 52 | **Together, we can equalify the internet.** 53 | -------------------------------------------------------------------------------- /actions/auth_callback.php: -------------------------------------------------------------------------------- 1 | exchange(ROUTE_URL_CALLBACK); 4 | 5 | // Finally, redirect our end user back to the / index route, to display their user profile: 6 | header("Location: " . ROUTE_URL_INDEX); 7 | exit; 8 | 9 | ?> -------------------------------------------------------------------------------- /actions/create_report.php: -------------------------------------------------------------------------------- 1 | prepare("INSERT INTO reports (report_title) VALUES (:report_title)"); 7 | 8 | // Bind the parameters 9 | $title = "Untitled Report"; 10 | $stmt->bindParam(':report_title', $title); 11 | 12 | // Execute the statement 13 | $stmt->execute(); 14 | 15 | // Get the last inserted ID 16 | $report_id = $pdo->lastInsertId(); 17 | 18 | // Redirect to the desired page with the report_id 19 | header("Location: ../index.php?view=report_settings&report_id=" . urlencode($report_id)); 20 | exit; 21 | 22 | ?> 23 | -------------------------------------------------------------------------------- /actions/delete_property.php: -------------------------------------------------------------------------------- 1 | beginTransaction(); 15 | 16 | // Delete related data from occurrences and its dependent tables 17 | $occurrencesStmt = $pdo->prepare("SELECT occurrence_id FROM occurrences WHERE occurrence_property_id = :property_id"); 18 | $occurrencesStmt->execute([':property_id' => $property_id]); 19 | $occurrence_ids = $occurrencesStmt->fetchAll(PDO::FETCH_COLUMN); 20 | 21 | if ($occurrence_ids) { 22 | // Delete from tag_relationships and updates table 23 | $pdo->exec("DELETE FROM tag_relationships WHERE occurrence_id IN (" . implode(',', $occurrence_ids) . ")"); 24 | $pdo->exec("DELETE FROM updates WHERE occurrence_id IN (" . implode(',', $occurrence_ids) . ")"); 25 | 26 | // Delete occurrences 27 | $pdo->exec("DELETE FROM occurrences WHERE occurrence_property_id = $property_id"); 28 | } 29 | 30 | // Delete from queued_scans and properties 31 | $pdo->exec("DELETE FROM queued_scans WHERE queued_scan_property_id = $property_id"); 32 | $pdo->exec("DELETE FROM properties WHERE property_id = $property_id"); 33 | $pdo->exec("DELETE FROM pages WHERE page_property_id = $property_id"); 34 | 35 | // Process report_filters in reports table 36 | $reportsStmt = $pdo->query("SELECT report_id, report_filters FROM reports"); 37 | $reports = $reportsStmt->fetchAll(PDO::FETCH_ASSOC); 38 | 39 | foreach ($reports as $report) { 40 | if(!empty($report['report_filters'])){ 41 | $filters = json_decode($report['report_filters'], true); 42 | 43 | // Check if filters contain the property and remove it 44 | $filtersModified = false; 45 | foreach ($filters as $key => $filter) { 46 | if ($filter['filter_type'] == 'properties' && $filter['filter_id'] == $property_id) { 47 | unset($filters[$key]); 48 | $filtersModified = true; 49 | } 50 | } 51 | 52 | // Update the report if filters were modified 53 | if ($filtersModified) { 54 | $updatedFiltersJson = json_encode(array_values($filters)); 55 | $updateStmt = $pdo->prepare("UPDATE reports SET report_filters = :filters WHERE report_id = :report_id"); 56 | $updateStmt->execute([ 57 | ':filters' => $updatedFiltersJson, 58 | ':report_id' => $report['report_id'] 59 | ]); 60 | } 61 | } 62 | } 63 | 64 | // Commit transaction 65 | $pdo->commit(); 66 | 67 | // Remove session token to prevent unintended submissions. 68 | $_SESSION['property_id'] = ''; 69 | 70 | // Success redirection or message 71 | $_SESSION['success'] = "Property and related data deleted."; 72 | header("Location: ../index.php?view=settings"); 73 | exit; 74 | 75 | } catch (Exception $e) { 76 | // Rollback transaction on error 77 | $pdo->rollBack(); 78 | 79 | // Error redirection or message 80 | $_SESSION['error'] = $e->getMessage(); 81 | header("Location: ../index.php?view=settings"); 82 | exit; 83 | } 84 | ?> 85 | -------------------------------------------------------------------------------- /actions/delete_report.php: -------------------------------------------------------------------------------- 1 | prepare("DELETE FROM reports WHERE report_id = :report_id"); 18 | 19 | // Bind the parameters 20 | $stmt->bindParam(':report_id', $report_id, PDO::PARAM_INT); 21 | 22 | // Execute the statement 23 | $stmt->execute(); 24 | 25 | // Remove session token to prevent unintended submissions. 26 | $_SESSION['report_id'] = ''; 27 | 28 | 29 | // Redirect after successful deletion 30 | $_SESSION['success'] = "Report deletion successful."; 31 | header("Location: ../index.php?view=reports"); 32 | exit; 33 | 34 | ?> 35 | -------------------------------------------------------------------------------- /actions/delete_report_filter_cookie.php: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /actions/install.php: -------------------------------------------------------------------------------- 1 | "CREATE TABLE `messages` ( 5 | `message_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 6 | `message_title` text NOT NULL, 7 | `message_link` text DEFAULT NULL, 8 | PRIMARY KEY (`message_id`) 9 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 10 | 11 | "meta" => "CREATE TABLE `meta` ( 12 | `meta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 13 | `meta_name` varchar(220) DEFAULT NULL, 14 | `meta_value` longtext DEFAULT NULL, 15 | PRIMARY KEY (`meta_id`) 16 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 17 | 18 | "occurrences" => "CREATE TABLE `occurrences` ( 19 | `occurrence_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 20 | `occurrence_message_id` bigint(20) NOT NULL, 21 | `occurrence_property_id` bigint(20) NOT NULL, 22 | `occurrence_status` varchar(220) NOT NULL, 23 | `occurrence_page_id` bigint(20) NOT NULL, 24 | `occurrence_source` varchar(220) NOT NULL, 25 | `occurrence_code_snippet` longtext DEFAULT NULL, 26 | `occurrence_archived` tinyint(1) DEFAULT NULL, 27 | PRIMARY KEY (`occurrence_id`) 28 | ) ENGINE=InnoDB AUTO_INCREMENT=67320 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 29 | 30 | "pages" => "CREATE TABLE `pages` ( 31 | `page_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 32 | `page_url` text NOT NULL, 33 | `page_property_id` bigint(20) NOT NULL, 34 | PRIMARY KEY (`page_id`) 35 | ) ENGINE=InnoDB AUTO_INCREMENT=3998 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 36 | 37 | "properties" => "CREATE TABLE `properties` ( 38 | `property_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 39 | `property_name` text NOT NULL, 40 | `property_archived` tinyint(1) DEFAULT NULL, 41 | `property_url` text NOT NULL, 42 | `property_processed` datetime DEFAULT NULL, 43 | `property_processing` tinyint(1) DEFAULT NULL, 44 | PRIMARY KEY (`property_id`) 45 | ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 46 | 47 | "queued_scans" => "CREATE TABLE `queued_scans` ( 48 | `queued_scan_job_id` bigint(20) NOT NULL, 49 | `queued_scan_property_id` bigint(20) NOT NULL, 50 | `queued_scan_page_id` bigint(20) DEFAULT NULL, 51 | `queued_scan_processing` tinyint(1) DEFAULT NULL, 52 | `queued_scan_prioritized` tinyint(1) DEFAULT NULL, 53 | PRIMARY KEY (`queued_scan_job_id`) 54 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 55 | 56 | "reports" => "CREATE TABLE `reports` ( 57 | `report_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 58 | `report_title` text NOT NULL, 59 | `report_visibility` varchar(220) DEFAULT NULL, 60 | `report_filters` text DEFAULT NULL, 61 | PRIMARY KEY (`report_id`) 62 | ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 63 | 64 | "tag_relationships" => "CREATE TABLE `tag_relationships` ( 65 | `occurrence_id` bigint(20) NOT NULL, 66 | `tag_id` bigint(20) unsigned NOT NULL, 67 | PRIMARY KEY (`occurrence_id`, `tag_id`) 68 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 69 | 70 | "tags" => "CREATE TABLE `tags` ( 71 | `tag_id` int(11) unsigned NOT NULL AUTO_INCREMENT, 72 | `tag_name` varchar(220) NOT NULL, 73 | `tag_slug` varchar(220) NOT NULL, 74 | PRIMARY KEY (`tag_id`) 75 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", 76 | 77 | "updates" => "CREATE TABLE `updates` ( 78 | `update_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 79 | `date_created` datetime NOT NULL, 80 | `occurrence_id` bigint(20) NOT NULL, 81 | `update_message` varchar(220) NOT NULL, 82 | PRIMARY KEY (`update_id`) 83 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;" 84 | ]; 85 | 86 | // Function to check and create table if it doesn't exist 87 | function checkAndCreateTable($pdo, $tableName, $createQuery) { 88 | $stmt = $pdo->query("SHOW TABLES LIKE '$tableName'"); 89 | if ($stmt->rowCount() == 0) { 90 | $pdo->exec($createQuery); 91 | } 92 | } 93 | 94 | // Loop through tables and check/create each 95 | foreach ($tables as $tableName => $createQuery) { 96 | checkAndCreateTable($pdo, $tableName, $createQuery); 97 | } -------------------------------------------------------------------------------- /actions/login.php: -------------------------------------------------------------------------------- 1 | clear(); 4 | 5 | // Finally, set up the local application session, and redirect the user to the Auth0 Universal Login Page to authenticate. 6 | header( "Location: " . $auth0->login( ROUTE_URL_CALLBACK ) ); 7 | exit; 8 | 9 | ?> 10 | -------------------------------------------------------------------------------- /actions/logout.php: -------------------------------------------------------------------------------- 1 | logout(ROUTE_URL_INDEX)); 4 | exit; 5 | ?> -------------------------------------------------------------------------------- /actions/process_page.php: -------------------------------------------------------------------------------- 1 | $page_url, 21 | "priortized" => true 22 | ) 23 | ); 24 | $ch = curl_init($api_url); 25 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 26 | curl_setopt($ch, CURLOPT_POST, true); 27 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 28 | curl_setopt($ch, CURLOPT_HTTPHEADER, array( 29 | 'Content-Type: application/json', 30 | 'Content-Length: ' . strlen($data) 31 | )); 32 | $response = curl_exec($ch); 33 | $response_data = json_decode($response, true); 34 | 35 | // Check if response data contains 'jobID' 36 | if (!isset($response_data['jobID'])){ 37 | $_SESSION['error'] = $e->getMessage("No response."); 38 | header("Location: ../index.php?view=page&page_id=$page_id&report_id=$report_id"); 39 | exit; 40 | } 41 | 42 | // Add results into scan queue. 43 | $stmt = $pdo->prepare("INSERT INTO queued_scans (queued_scan_job_id, queued_scan_property_id, queued_scan_page_id, queued_scan_prioritized) VALUES (:queued_scan_job_id, :queued_scan_property_id, :queued_scan_page_id, :queued_scan_prioritized)"); 44 | $stmt->bindParam(':queued_scan_job_id', $response_data['jobID'], PDO::PARAM_INT); 45 | $stmt->bindParam(':queued_scan_property_id', $page_property_id, PDO::PARAM_INT); 46 | $stmt->bindParam(':queued_scan_page_id', $page_id, PDO::PARAM_INT); 47 | $stmt->bindValue(':queued_scan_prioritized', 1, PDO::PARAM_INT); 48 | $stmt->execute(); 49 | 50 | // Remove session. 51 | $_SESSION['process_this_page'] = ''; 52 | 53 | // Set success messsage as session. 54 | $_SESSION['success'] = "$page_url queued for scanning."; 55 | 56 | // Redirect 57 | header("Location: ../index.php?view=scans"); 58 | exit; -------------------------------------------------------------------------------- /actions/process_property.php: -------------------------------------------------------------------------------- 1 | getMessage(); 85 | if(isset($_SESSION['property_id'])) 86 | unset($_SESSION['property_id']); 87 | exit; 88 | 89 | } 90 | 91 | function get_api_results($property_url) { 92 | 93 | // Set API endpoint 94 | $api_url = $_ENV['SCAN_URL'].'/generate/sitemapurl'; 95 | 96 | // Prepare the payload 97 | $data = json_encode(array("url" => $property_url)); 98 | 99 | // Initialize cURL session 100 | $ch = curl_init($api_url); 101 | 102 | // Set cURL options 103 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 104 | curl_setopt($ch, CURLOPT_POST, true); 105 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 106 | curl_setopt($ch, CURLOPT_HTTPHEADER, array( 107 | 'Content-Type: application/json', 108 | 'Content-Length: ' . strlen($data) 109 | )); 110 | 111 | // Execute cURL session 112 | $response = curl_exec($ch); 113 | 114 | // Check for errors 115 | if(curl_errno($ch)){ 116 | throw new Exception(curl_error($ch)); 117 | } 118 | 119 | // Close cURL session 120 | curl_close($ch); 121 | 122 | // Decode JSON response 123 | $results = json_decode($response, true); 124 | 125 | return $results; 126 | } 127 | 128 | function find_page_id($url, $propertyId) { 129 | global $pdo; 130 | 131 | $sql = "SELECT page_id FROM pages WHERE page_url = :url AND page_property_id = :propertyId LIMIT 1"; 132 | $stmt = $pdo->prepare($sql); 133 | $stmt->bindParam(':url', $url, PDO::PARAM_STR); 134 | $stmt->bindParam(':propertyId', $propertyId, PDO::PARAM_INT); 135 | $stmt->execute(); 136 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 137 | 138 | return $result ? $result['page_id'] : null; 139 | } 140 | 141 | 142 | function update_property_processing_data($property_id, $property_processing = NULL) { 143 | global $pdo; 144 | $current_date_time = date('Y-m-d H:i:s'); 145 | 146 | $update_query = " 147 | UPDATE properties 148 | SET 149 | property_processing = :property_processing, 150 | property_processed = :property_processed 151 | WHERE 152 | property_id = :property_id 153 | "; 154 | 155 | $update_stmt = $pdo->prepare($update_query); 156 | $update_stmt->execute([ 157 | ':property_processing' => $property_processing, 158 | ':property_processed' => $current_date_time, // Set the current date and time 159 | ':property_id' => $property_id 160 | ]); 161 | } 162 | 163 | function results_are_valid_format($results) { 164 | 165 | // First check if JSON decoding was successful and is an array 166 | if ($results === null || !is_array($results)) { 167 | throw new Exception("Property results are not formatted correctly"); 168 | } 169 | 170 | // Validate each element in the array 171 | foreach ($results as $item) { 172 | if (!isset($item['JobID']) || !isset($item['URL'])) { 173 | throw new Exception("$item"); 174 | } 175 | } 176 | 177 | // On sucesss 178 | return true; 179 | 180 | } 181 | 182 | function save_to_database($results, $property_id) { 183 | global $pdo; 184 | 185 | // Extract all job IDs from the results 186 | $jobIds = array_column($results, 'JobID'); 187 | 188 | // Check which job IDs already exist in the database 189 | $placeholders = implode(',', array_fill(0, count($jobIds), '?')); 190 | $checkQuery = "SELECT queued_scan_job_id FROM queued_scans WHERE queued_scan_job_id IN ($placeholders)"; 191 | $checkStmt = $pdo->prepare($checkQuery); 192 | $checkStmt->execute($jobIds); 193 | $existingJobIds = $checkStmt->fetchAll(PDO::FETCH_COLUMN, 0); 194 | 195 | // Filter out existing job IDs from results 196 | $newResults = array_filter($results, function($result) use ($existingJobIds) { 197 | return !in_array($result['JobID'], $existingJobIds); 198 | }); 199 | 200 | // Batch insert the new results 201 | if (!empty($newResults)) { 202 | $insertQuery = "INSERT INTO queued_scans (queued_scan_job_id, queued_scan_property_id, queued_scan_page_id) VALUES "; 203 | $insertValues = []; 204 | $params = []; 205 | foreach ($newResults as $index => $result) { 206 | $insertValues[] = "(:jobId{$index}, :propertyId{$index}, :page_id{$index})"; 207 | $params[":jobId{$index}"] = $result['JobID']; 208 | $params[":propertyId{$index}"] = $property_id; 209 | $params[":page_id{$index}"] = $result['page_id']; 210 | } 211 | 212 | $insertQuery .= implode(', ', $insertValues); 213 | $insertStmt = $pdo->prepare($insertQuery); 214 | $insertStmt->execute($params); 215 | } 216 | } 217 | ?> -------------------------------------------------------------------------------- /actions/process_scans.php: -------------------------------------------------------------------------------- 1 | $job_id, 24 | 'queued_scan_property_id' => $property_id, 25 | ] 26 | ]; 27 | process_scans($scans); 28 | } else { 29 | $error_message = 'Invalid scan parameters'; 30 | $_SESSION['error'] = $error_message; 31 | header("Location: ../index.php?view=scans"); 32 | exit; 33 | } 34 | 35 | // No arguements mean we process everything. 36 | } else { 37 | process_scans(); 38 | } 39 | 40 | // The script can also be run by posting to it or via custom URL variables 41 | }elseif ( 42 | (isset($_POST['job_id']) && isset($_POST['property_id'])) || 43 | (isset($_GET['job_id']) && isset($_GET['property_id'])) 44 | ) { 45 | 46 | // Validate and sanitize inputs 47 | if(isset($_POST['job_id'])) 48 | $job_id = filter_var($_POST['job_id'], FILTER_VALIDATE_INT); 49 | if(isset($_POST['property_id'])) 50 | $property_id = filter_var($_POST['property_id'], FILTER_VALIDATE_INT); 51 | if(isset($_GET['job_id'])) 52 | $job_id = $_GET['job_id']; 53 | if(isset($_GET['property_id'])) 54 | $property_id = $_GET['property_id']; 55 | 56 | if ($job_id !== false && $property_id !== false) { 57 | $scans = [ 58 | [ 59 | 'queued_scan_job_id' => $job_id, 60 | 'queued_scan_property_id' => $property_id, 61 | ] 62 | ]; 63 | process_scans($scans); 64 | } else { 65 | $error_message = 'Invalid scan parameters'; 66 | update_log($error_message); 67 | $_SESSION['error'] = $error_message; 68 | header("Location: ../index.php?view=scans"); 69 | exit; 70 | } 71 | 72 | } else { 73 | 74 | // Not a POST request. Could be CLI or other 75 | // Existing code for processing scans 76 | process_scans(); 77 | 78 | } 79 | 80 | } catch (Exception $e) { 81 | 82 | // Handle the exception 83 | echo $e->getMessage(); 84 | exit; 85 | 86 | } 87 | 88 | // Helper Functions 89 | function process_scans($scans = null) { 90 | global $pdo; 91 | 92 | $max_scans = $_ENV['CONCURRENT_SCANS'] ?? 20; // Set maximum concurrent scans, default to 20 (what axe can do on xxs machine) 93 | 94 | // If scans aren't declared, this will just get multiple 95 | // scans automatically. 96 | if ($scans === null) { 97 | 98 | // Fetch up prioritized scans 99 | $stmt = $pdo->prepare("SELECT queued_scan_job_id, queued_scan_property_id FROM queued_scans WHERE queued_scan_prioritized = 1 LIMIT $max_scans;"); 100 | $stmt->execute(); 101 | $prioritized_scans = $stmt->fetchAll(PDO::FETCH_ASSOC); 102 | 103 | // Just use prioritized scans if there's enough 104 | if(count($prioritized_scans) >= $max_scans) 105 | $scans = $prioritized_scans; 106 | 107 | // If not enough prioritized scans fetch the next scan 108 | if (count($prioritized_scans) < $max_scans || empty($scans)) { 109 | $scan_limit = $max_scans - count($prioritized_scans); 110 | $stmt = $pdo->prepare("SELECT queued_scan_job_id, queued_scan_property_id FROM queued_scans WHERE queued_scan_processing IS NULL AND queued_scan_prioritized IS NULL LIMIT $scan_limit;"); 111 | $stmt->execute(); 112 | $other_scans = $stmt->fetchAll(PDO::FETCH_ASSOC); 113 | $scans = array_merge($prioritized_scans, $other_scans); 114 | } 115 | 116 | } 117 | 118 | // Handle if no scans to process. 119 | if (empty($scans)) { 120 | 121 | // Stop process if there is no scan. 122 | $error_message = 'No scans to process.'; 123 | update_log($error_message); 124 | $_SESSION['error'] = $error_message; 125 | header("Location: ../index.php?view=scans"); 126 | exit; 127 | 128 | } 129 | 130 | // Run API on each scan 131 | $logged_messages = array(); 132 | foreach ($scans as $scan): 133 | 134 | // Define property id and job id. 135 | $property_id = $scan['queued_scan_property_id']; 136 | $job_id = $scan['queued_scan_job_id']; 137 | 138 | // Set the scan as processing 139 | update_processing_value($job_id, 1); 140 | 141 | // Perform the API GET request 142 | $api_url = $_ENV['SCAN_URL']. '/results/axe/' . $job_id; 143 | $json = file_get_contents($api_url); 144 | 145 | // Handle scans that don't return JSON. 146 | if ($json === false) { 147 | $message = "Scan $job_id returns no JSON. Scan deleted."; 148 | $logged_messages.=$message.'
'; 149 | update_log($message); 150 | delete_scan($job_id); 151 | continue; 152 | } 153 | 154 | // Decode the JSON response 155 | $data = json_decode($json, true); 156 | 157 | // Handle incomplete scans. 158 | $statuses = array('delayed', 'active', 'waiting'); 159 | if(in_array($data['status'], $statuses)){ 160 | $message = 'Scan ' . $job_id . ' has "' . $data['status'] .'" status. Scan skipped.'; 161 | $logged_messages.=$message.'
'; 162 | update_log($message); 163 | update_processing_value($job_id, NULL); 164 | continue; 165 | } 166 | 167 | // Handle problems scans. 168 | $statuses = array('failed', 'unknown'); 169 | if(in_array($data['status'], $statuses)){ 170 | $message = 'Scan ' . $job_id . ' has "' . $data['status'] .'" status. Scan skipped.'; 171 | $logged_messages.=$message.'
'; 172 | update_log($message); 173 | delete_scan($job_id); 174 | continue; 175 | } 176 | 177 | // Setup variables from decoded json 178 | $new_occurrences = []; 179 | $page_url = $data['result']['results']['url'] ?? ''; 180 | 181 | // Setup page id 182 | $page_id = get_page_id($page_url, $property_id); 183 | 184 | // Check if violations are formatted correctly and set them up 185 | if (isset($data['result']['results']['violations']) && !empty($data['result']['results']['violations'])) { 186 | foreach ($data['result']['results']['violations'] as $violation) { 187 | 188 | // Handle incorrectly formatted violations 189 | if (!isset($violation['id'], $violation['tags'], $violation['nodes'])) { 190 | $message = "Scan $job_id returns violations in invalid format. Scan deleted."; 191 | $logged_messages.=$message.'
'; 192 | update_log($message); 193 | delete_scan($job_id); 194 | continue; 195 | } 196 | 197 | // Handle More Info URL 198 | $message_link = $violation['helpUrl']; 199 | 200 | foreach ($violation['nodes'] as $node) { 201 | 202 | // Handle incorrectly formatted nodes. 203 | if (!isset($node['html'])) { 204 | $message = "Scan $job_id returns node in invalid format. Scan deleted."; 205 | $logged_messages.=$message.'
'; 206 | update_log($message); 207 | 208 | delete_scan($job_id); 209 | continue; 210 | } 211 | foreach (['any', 'all', 'none'] as $key) { 212 | if (isset($node[$key]) && is_array($node[$key])) { 213 | foreach ($node[$key] as $item) { 214 | 215 | // Handle incorrectly formatted messages. 216 | if (!isset($item['message'])) { 217 | $message = "Scan $job_id returns invalid '$key' format in node. Scan deleted."; 218 | $logged_messages.=$message.'
'; 219 | update_log($message); 220 | 221 | delete_scan($job_id); 222 | continue; 223 | } 224 | 225 | // Construct the occurrence data 226 | $new_occurrences[] = [ 227 | "occurrence_message_id" => get_message_id($item['message'], $message_link), 228 | "occurrence_code_snippet" => $node['html'], 229 | "occurrence_page_id" => $page_id, 230 | "occurrence_source" => "scan.equalify.app", 231 | "occurrence_property_id" => $property_id, 232 | "tag_ids" => get_tag_ids($violation['tags']) 233 | ]; 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | // Handle unformatted results. 241 | }else{ 242 | $message = "Scan $job_id returns no violations. Scan deleted."; 243 | $logged_messages[] = $message; 244 | update_log($message); 245 | delete_scan($job_id); 246 | continue; 247 | } 248 | 249 | // Group occurrences by page_id and source 250 | $grouped_occurrences = []; 251 | foreach ($new_occurrences as $occurrence) { 252 | $key = $occurrence['occurrence_page_id'] . '_' . $occurrence['occurrence_source']; 253 | $grouped_occurrences[$key][] = $occurrence; 254 | } 255 | 256 | // If no new occurrences are found, add a dummy group to trigger the database check 257 | if (empty($new_occurrences)) { 258 | $grouped_occurrences[$page_id . '_scan.equalify.app'] = []; 259 | } 260 | 261 | $reactivated_occurrences = []; 262 | $equalified_occurrences = []; 263 | $to_save_occurrences = []; 264 | 265 | foreach ($grouped_occurrences as $key => $group) { 266 | list($page_id, $source) = explode('_', $key); 267 | 268 | // Fetch existing occurrences from database 269 | $existing_occurrences_stmt = $pdo->prepare("SELECT * FROM occurrences WHERE occurrence_page_id = ? AND occurrence_source = ?"); 270 | $existing_occurrences_stmt->execute([$page_id, $source]); 271 | $existing_occurrences = $existing_occurrences_stmt->fetchAll(PDO::FETCH_ASSOC); 272 | 273 | $existing_ids_in_group = []; 274 | 275 | // Check if each new occurrence exists in the database 276 | foreach ($group as $occurrence) { 277 | $found = false; 278 | foreach ($existing_occurrences as $existing_occurrence) { 279 | if ($existing_occurrence['occurrence_code_snippet'] == $occurrence['occurrence_code_snippet'] && 280 | $existing_occurrence['occurrence_message_id'] == $occurrence['occurrence_message_id']) { 281 | $found = true; 282 | $existing_ids_in_group[] = $existing_occurrence['occurrence_id']; 283 | if ($existing_occurrence['occurrence_status'] == 'equalified') { 284 | $reactivated_occurrences[] = $existing_occurrence['occurrence_id']; 285 | } 286 | break; 287 | } 288 | } 289 | 290 | if (!$found) { 291 | $to_save_occurrences[] = $occurrence; 292 | } 293 | } 294 | 295 | // Mark as 'equalified' occurrences that are in the database without the status "equalfied" but not in new occurrences 296 | foreach ($existing_occurrences as $existing_occurrence) { 297 | if (!in_array($existing_occurrence['occurrence_id'], $existing_ids_in_group) && $existing_occurrence['occurrence_status'] !== 'equalified') { 298 | $equalified_occurrences[] = $existing_occurrence['occurrence_id']; 299 | } 300 | } 301 | 302 | } 303 | 304 | // Save new occurrences as 'activated' 305 | $new_occurrence_ids = []; 306 | $new_occurrence_tag_relationships = []; 307 | foreach ($to_save_occurrences as $occurrence) { 308 | 309 | // Insert occurrences into db. 310 | $insert_stmt = $pdo->prepare("INSERT INTO occurrences (occurrence_message_id, occurrence_code_snippet, occurrence_page_id, occurrence_source, occurrence_property_id, occurrence_status) VALUES (?, ?, ?, ?, ?, 'active')"); 311 | $insert_stmt->execute([ 312 | $occurrence['occurrence_message_id'], 313 | $occurrence['occurrence_code_snippet'], 314 | $occurrence['occurrence_page_id'], 315 | $occurrence['occurrence_source'], 316 | $occurrence['occurrence_property_id'] 317 | ]); 318 | $new_occurrence_ids[] = $pdo->lastInsertId(); 319 | $new_occurrence_tag_relationships[] = array( 320 | 'occurrence_id' => $pdo->lastInsertId(), 321 | 'occurrence_tag_ids' => $occurrence['tag_ids'] 322 | ); 323 | 324 | } 325 | 326 | // Insert tags relationships into db 327 | add_tag_relationships($new_occurrence_tag_relationships); 328 | 329 | // Count occurrences for logging 330 | $count_reactivated_occurrences = count($reactivated_occurrences); 331 | $count_equalified_occurrences = count($equalified_occurrences); 332 | $count_new_occurrence_ids = count($new_occurrence_ids); 333 | 334 | // Update statuses in the database 335 | $update_stmt = $pdo->prepare("UPDATE occurrences SET occurrence_status = ? WHERE occurrence_id = ?"); 336 | foreach ($reactivated_occurrences as $id) { 337 | $update_stmt->execute(['active', $id]); 338 | } 339 | foreach ($equalified_occurrences as $id) { 340 | $update_stmt->execute(['equalified', $id]); 341 | } 342 | 343 | // Insert updates for new and reactivated occurrences 344 | $insert_update_stmt = $pdo->prepare("INSERT INTO updates (date_created, occurrence_id, update_message) VALUES (NOW(), ?, ?)"); 345 | foreach (array_merge($new_occurrence_ids, $reactivated_occurrences) as $id) { 346 | $insert_update_stmt->execute([$id, 'activated']); 347 | } 348 | 349 | // Insert updates for equalified occurrences 350 | foreach ($equalified_occurrences as $id) { 351 | $insert_update_stmt->execute([$id, 'equalified']); 352 | } 353 | 354 | // On success delete scan 355 | delete_scan($job_id); 356 | 357 | // Log output. 358 | $message = "Scan $job_id successfully processed. $count_new_occurrence_ids new. $count_equalified_occurrences equalified. $count_reactivated_occurrences reactivated."; 359 | $logged_messages[] = $message; 360 | update_log($message); 361 | 362 | // End scan processing 363 | endforeach; 364 | 365 | // Redirect with logged messages 366 | if(!empty($logged_messages)){ 367 | $success_message = 'Success! Returned the following results: "; 372 | $_SESSION['success'] = $success_message; 373 | header("Location: ../index.php?view=scans"); 374 | } 375 | } 376 | 377 | function update_processing_value($job_id, $new_value){ 378 | global $pdo; 379 | 380 | $stmt = $pdo->prepare("UPDATE queued_scans SET queued_scan_processing = ? WHERE queued_scan_job_id = ?"); 381 | $stmt->execute([$new_value, $job_id]); 382 | } 383 | 384 | function get_message_id($title, $message_link) { 385 | global $pdo; 386 | 387 | // Check if the message exists 388 | $query = "SELECT message_id FROM messages WHERE message_title = :title"; 389 | $stmt = $pdo->prepare($query); 390 | $stmt->execute([':title' => $title]); 391 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 392 | 393 | if ($row) { 394 | return $row['message_id']; // Return existing ID 395 | } else { 396 | // Insert the new message 397 | $insertQuery = "INSERT INTO messages (message_title, message_link) VALUES (:title, :message_link)"; 398 | $insertStmt = $pdo->prepare($insertQuery); 399 | $insertStmt->execute([':title' => $title, ':message_link' => $message_link]); 400 | return $pdo->lastInsertId(); // Return new ID 401 | } 402 | } 403 | 404 | function get_page_id($url, $property_id) { 405 | global $pdo; 406 | 407 | // Check if the page exists 408 | $pageQuery = "SELECT page_id FROM pages WHERE page_url = :url AND page_property_id = :property_id"; 409 | $pageStmt = $pdo->prepare($pageQuery); 410 | $pageStmt->execute([':url' => $url, ':property_id' => $property_id]); 411 | $pageRow = $pageStmt->fetch(PDO::FETCH_ASSOC); 412 | 413 | if ($pageRow) { 414 | return $pageRow['page_id']; // Return existing ID 415 | } else { 416 | // Insert the new page 417 | $insertPageQuery = "INSERT INTO pages (page_url, page_property_id) VALUES (:url, :property_id)"; 418 | $insertPageStmt = $pdo->prepare($insertPageQuery); 419 | $insertPageStmt->execute([':url' => $url, ':property_id' => $property_id]); 420 | return $pdo->lastInsertId(); // Return new ID 421 | } 422 | } 423 | 424 | function get_tag_ids($tags) { 425 | global $pdo; 426 | $tagIds = []; 427 | 428 | foreach ($tags as $tag) { 429 | $sanitizedTagSlug = preg_replace('/[^a-z0-9-]+/', '-', strtolower($tag)); // Sanitize tag slug 430 | 431 | // Check if the tag exists 432 | $tagQuery = "SELECT tag_id FROM tags WHERE tag_name = :tag"; 433 | $tagStmt = $pdo->prepare($tagQuery); 434 | $tagStmt->execute([':tag' => $tag]); 435 | $tagRow = $tagStmt->fetch(PDO::FETCH_ASSOC); 436 | 437 | if ($tagRow) { 438 | $tagIds[] = (int)$tagRow['tag_id']; 439 | } else { 440 | // Insert the new tag 441 | $insertTagQuery = "INSERT INTO tags (tag_name, tag_slug) VALUES (:tag, :slug)"; 442 | $insertTagStmt = $pdo->prepare($insertTagQuery); 443 | $insertTagStmt->execute([':tag' => $tag, ':slug' => $sanitizedTagSlug]); 444 | $tagIds[] = (int)$pdo->lastInsertId(); 445 | } 446 | } 447 | 448 | return $tagIds; // Return concatenated tag IDs 449 | } 450 | 451 | function add_tag_relationships($new_occurrence_tag_relationships) { 452 | global $pdo; 453 | 454 | // Start transaction 455 | $pdo->beginTransaction(); 456 | 457 | try { 458 | $query = "INSERT INTO tag_relationships (tag_id, occurrence_id) VALUES "; 459 | 460 | $insertValues = []; 461 | $params = []; 462 | $index = 0; 463 | 464 | foreach ($new_occurrence_tag_relationships as $tag_relationship) { 465 | foreach ($tag_relationship['occurrence_tag_ids'] as $tag_id) { 466 | $insertValues[] = "(:tag_id{$index}, :occurrence_id{$index})"; 467 | $params[":tag_id{$index}"] = $tag_id; 468 | $params[":occurrence_id{$index}"] = $tag_relationship['occurrence_id']; 469 | $index++; 470 | } 471 | } 472 | 473 | if(!empty($insertValues)) { 474 | $query .= implode(', ', $insertValues); 475 | $statement = $pdo->prepare($query); 476 | $statement->execute($params); 477 | } 478 | 479 | // Commit the transaction 480 | $pdo->commit(); 481 | } catch (PDOException $e) { 482 | // Rollback the transaction on error 483 | $pdo->rollBack(); 484 | throw $e; 485 | } 486 | } 487 | 488 | function delete_scan($job_id){ 489 | 490 | global $pdo; 491 | 492 | $query = "DELETE FROM queued_scans WHERE queued_scan_job_id = :queued_scan_job_id"; 493 | $statement = $pdo->prepare($query); 494 | $statement->execute([':queued_scan_job_id' => $job_id]); 495 | 496 | } 497 | 498 | function update_log($message){ 499 | echo(date('Y-m-d H:i:s') . ": $message\n"); 500 | } 501 | ?> 502 | -------------------------------------------------------------------------------- /actions/queue_report_filter_change.php: -------------------------------------------------------------------------------- 1 | $filter_data['filter_type'], 37 | 'filter_value' => $filter_data['filter_value'], 38 | 'filter_id' => $filter_data['filter_id'], 39 | 'filter_change' => $filter_data['filter_change'] ?? 'add' // Default to 'add' if not specified 40 | ]; 41 | } 42 | } 43 | 44 | // Update the cookie with the latest filters 45 | setcookie($cookie_name, json_encode(array_values($latest_filters)), time() + strtotime( '+30 days' ), '/'); 46 | 47 | // Redirect the user to the report page with the report ID 48 | session_start(); 49 | $session_message = "Successfully changed filters."; 50 | $_SESSION['success'] = $session_message; 51 | header("Location: ../index.php?view=report_settings&report_id=" . urlencode($report_id)); 52 | exit; 53 | ?> 54 | -------------------------------------------------------------------------------- /actions/save_property_settings.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE properties SET property_name = :property_name, property_url = :property_url, property_archived = :property_archived, property_processed = :property_processed WHERE property_id = :property_id"); 40 | 41 | // Bind the parameters 42 | $stmt->bindParam(':property_name', $_POST['property_name'], PDO::PARAM_STR); 43 | $stmt->bindParam(':property_url', $_POST['property_url'], PDO::PARAM_STR); 44 | $stmt->bindParam(':property_archived', $property_archived, PDO::PARAM_INT); 45 | $stmt->bindParam(':property_processed', $property_processed, PDO::PARAM_STR); 46 | $stmt->bindParam(':property_id', $property_id, PDO::PARAM_INT); 47 | 48 | // Execute the statement 49 | $stmt->execute(); 50 | 51 | }else{ 52 | 53 | // New property statement 54 | $stmt = $pdo->prepare("INSERT INTO properties (property_name, property_url, property_archived) VALUES (:property_name, :property_url, :property_archived)"); 55 | 56 | // Bind the parameters 57 | $stmt->bindParam(':property_name', $_POST['property_name'], PDO::PARAM_STR); 58 | $stmt->bindParam(':property_url', $_POST['property_url'], PDO::PARAM_STR); 59 | $stmt->bindParam(':property_archived', $property_archived, PDO::PARAM_INT); 60 | 61 | // Execute the insert statement 62 | $stmt->execute(); 63 | 64 | // Get the ID of the last inserted record 65 | $property_id = $pdo->lastInsertId(); 66 | 67 | } 68 | 69 | // Remove session token to prevent unintended submissions. 70 | $_SESSION['property_id'] = ''; 71 | 72 | // Redirect on success 73 | $_SESSION['success'] = '"'.$_POST['property_name'].'" property saved.'; 74 | header("Location: ../index.php?view=property_settings&property_id=$property_id"); 75 | exit; 76 | 77 | } catch (Exception $e) { 78 | 79 | // Remove session token to prevent unintended submissions. 80 | $_SESSION['property_id'] = ''; 81 | 82 | // Handle any errors 83 | $_SESSION['error'] = $e->getMessage(); 84 | header("Location: ../index.php?view=property_settings&property_id=$property_id"); 85 | 86 | exit; 87 | } 88 | ?> 89 | -------------------------------------------------------------------------------- /actions/save_report_filter_change.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE reports SET report_filters = :report_filters WHERE report_id = :report_id"); 19 | $updated_filters_json = json_encode($report_filters); 20 | $update_stmt->bindParam(':report_filters', $updated_filters_json); 21 | $update_stmt->bindParam(':report_id', $report_id, PDO::PARAM_INT); 22 | $update_stmt->execute(); 23 | 24 | // Remove the cookie 25 | $cookie_name = 'queue_report_' . $report_id . '_filter_change'; 26 | setcookie($cookie_name, '', time() + strtotime( '+30 days' ), '/'); 27 | 28 | // Redirect the user to the report page with the report ID 29 | $session_message = "Report filters saved for everyone."; 30 | $_SESSION['success'] = $session_message; 31 | header("Location: ../index.php?&view=report&report_id=$report_id"); 32 | exit; 33 | 34 | ?> -------------------------------------------------------------------------------- /actions/save_report_title.php: -------------------------------------------------------------------------------- 1 | prepare("UPDATE reports SET report_title = :report_title WHERE report_id = :report_id"); 19 | 20 | // Bind the parameters 21 | $stmt->bindParam(':report_title', $_POST['report_title'], PDO::PARAM_STR); 22 | $stmt->bindParam(':report_id', $report_id, PDO::PARAM_INT); 23 | 24 | // Execute the statement 25 | $stmt->execute(); 26 | 27 | // Remove session token to prevent unintended submissions. 28 | $_SESSION['report_id'] = ''; 29 | 30 | // Redirect on success 31 | $_SESSION['success'] = "Title updated successfully."; 32 | header("Location: ../index.php?view=report_settings&report_id=" . urlencode($report_id)); 33 | exit; 34 | 35 | } catch (Exception $e) { 36 | // Handle any errors 37 | $_SESSION['error'] = $e->getMessage(); 38 | header("Location: ../index.php?view=report_settings&report_id=" . urlencode($report_id)); 39 | 40 | exit; 41 | } 42 | ?> 43 | -------------------------------------------------------------------------------- /api/index.php: -------------------------------------------------------------------------------- 1 | 'Invalid request type']; 53 | } 54 | 55 | echo json_encode($data); -------------------------------------------------------------------------------- /api/requests/chart.php: -------------------------------------------------------------------------------- 1 | prepare($occurrence_ids_query); 33 | $stmt->execute(); 34 | $occurrence_ids = $stmt->fetchAll(PDO::FETCH_ASSOC); 35 | $occurrence_id_list = implode( 36 | ',', 37 | array_map( 38 | function ($row) { 39 | return $row['occurrence_id']; 40 | }, 41 | $occurrence_ids 42 | ) 43 | ); 44 | 45 | // Construct the SQL query with filters 46 | if(!empty($occurrence_id_list)){ 47 | $updates_sql = " 48 | SELECT 49 | month_year, 50 | update_message, 51 | occurrence_id 52 | FROM ( 53 | SELECT 54 | DATE_FORMAT(u.date_created, '%Y-%m') AS month_year, 55 | u.update_message, 56 | o.occurrence_id, 57 | ROW_NUMBER() OVER(PARTITION BY o.occurrence_id, DATE_FORMAT(u.date_created, '%Y-%m') ORDER BY u.date_created DESC) as rn 58 | FROM 59 | updates u 60 | JOIN occurrences o ON u.occurrence_id = o.occurrence_id 61 | WHERE 62 | o.occurrence_id IN ($occurrence_id_list) 63 | "; 64 | 65 | if (!empty($filters['statuses'])) { 66 | $statuses = is_array($filters['statuses']) ? $filters['statuses'] : explode(',', $filters['statuses']); 67 | $statuses = array_map(function($status) { 68 | return $status === 'active' ? 'activated' : preg_replace("/[^a-zA-Z0-9_\-]+/", "", $status); 69 | }, $statuses); 70 | $statusList = "'" . implode("', '", $statuses) . "'"; 71 | $updates_sql .= " AND u.update_message IN ($statusList)"; 72 | } 73 | 74 | $updates_sql .= " 75 | ) as sub 76 | WHERE rn = 1 77 | GROUP BY month_year, update_message, occurrence_id 78 | ORDER BY month_year ASC 79 | "; 80 | 81 | // Updates query 82 | $stmt = $pdo->prepare($updates_sql); 83 | $stmt->execute(); 84 | $updates = $stmt->fetchAll(PDO::FETCH_ASSOC); 85 | 86 | }else{ 87 | $updates = ''; 88 | } 89 | 90 | if (empty($updates)) { 91 | // Handle the case where there are no results (perhaps return an empty chart or a default chart) 92 | return [ 93 | 'labels' => [], 94 | 'datasets' => [ 95 | ['label' => 'Equalified', 'data' => [], 'borderColor' => 'green', 'fill' => false], 96 | ['label' => 'Active', 'data' => [], 'borderColor' => 'red', 'fill' => false], 97 | ['label' => 'Ignored', 'data' => [], 'borderColor' => 'gray', 'fill' => false] 98 | ] 99 | ]; 100 | } 101 | 102 | // Find the earliest and latest dates 103 | $minDate = min(array_column($updates, 'month_year')); 104 | $maxDate = max(array_column($updates, 'month_year')); 105 | 106 | // Generate a list of all months between minDate and maxDate 107 | $period = new DatePeriod( 108 | new DateTime($minDate), 109 | new DateInterval('P1M'), 110 | (new DateTime($maxDate))->modify('+1 month') 111 | ); 112 | 113 | $months = []; 114 | foreach ($period as $date) { 115 | $months[$date->format('Y-m')] = ['equalified' => 0, 'activated' => 0, 'ignored' => 0]; 116 | } 117 | 118 | // Track the latest status of each occurrence 119 | $occurrenceStatus = []; 120 | 121 | foreach ($updates as $row) { 122 | $monthYear = $row['month_year']; 123 | $status = $row['update_message']; 124 | $occurrenceId = $row['occurrence_id']; 125 | 126 | // Update the occurrence status 127 | $previousStatus = $occurrenceStatus[$occurrenceId] ?? null; 128 | $occurrenceStatus[$occurrenceId] = $status; 129 | 130 | // Update counts for this month and future months 131 | foreach ($months as $month => &$counts) { 132 | if ($month < $monthYear) { 133 | continue; 134 | } 135 | 136 | // Add to the new status count 137 | $counts[$status]++; 138 | 139 | // If this occurrence was previously counted under a different status, subtract one from that status 140 | if ($previousStatus && $previousStatus != $status) { 141 | $counts[$previousStatus]--; 142 | } 143 | } 144 | } 145 | 146 | // Prepare labels and datasets 147 | $labels = array_keys($months); 148 | $equalified_data = array_column($months, 'equalified'); 149 | $active_data = array_column($months, 'activated'); 150 | $ignored_data = array_column($months, 'ignored'); 151 | 152 | $datasets = []; 153 | 154 | // Equalified dataset 155 | $datasets[] = [ 156 | 'label' => 'Equalified', 157 | 'data' => $equalified_data, 158 | 'borderColor' => 'green', 159 | 'fill' => false 160 | ]; 161 | 162 | // Active dataset 163 | $datasets[] = [ 164 | 'label' => 'Active', 165 | 'data' => $active_data, 166 | 'borderColor' => 'red', 167 | 'fill' => false 168 | ]; 169 | 170 | // Ignored dataset (if needed) 171 | $datasets[] = [ 172 | 'label' => 'Ignored', 173 | 'data' => $ignored_data, 174 | 'borderColor' => 'gray', 175 | 'fill' => false 176 | ]; 177 | 178 | $chart = [ 179 | 'labels' => $labels, 180 | 'datasets' => $datasets 181 | ]; 182 | 183 | return $chart; 184 | } -------------------------------------------------------------------------------- /api/requests/messages.php: -------------------------------------------------------------------------------- 1 | query($count_sql); 56 | return $stmt->fetchColumn(); 57 | } 58 | 59 | function fetch_messages( $results_per_page, $offset, $filters = []) { 60 | global $pdo; 61 | 62 | $whereClauses = build_where_clauses($filters); 63 | $joinClauses = build_join_clauses($filters); 64 | 65 | $sql = " 66 | SELECT 67 | m.message_id, 68 | m.message_title, 69 | SUM(o.occurrence_status = 'equalified') AS equalified_count, 70 | SUM(o.occurrence_status = 'active') AS active_count, 71 | SUM(o.occurrence_status = 'ignored') AS ignored_count, 72 | COUNT(o.occurrence_id) AS total_count 73 | FROM 74 | messages m 75 | INNER JOIN 76 | occurrences o ON m.message_id = o.occurrence_message_id 77 | $joinClauses 78 | $whereClauses 79 | GROUP BY m.message_id 80 | ORDER BY SUM(o.occurrence_status = 'active') DESC, m.message_id 81 | LIMIT $results_per_page OFFSET $offset 82 | "; 83 | 84 | $stmt = $pdo->query($sql); 85 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 86 | } 87 | 88 | function get_results($results_per_page, $offset, $filters = []) { 89 | $total_messages = count_total_messages($filters); 90 | $messages = fetch_messages($results_per_page, $offset, $filters); 91 | $total_pages = ceil($total_messages / $results_per_page); 92 | 93 | return [ 94 | 'messages' => $messages, 95 | 'totalPages' => $total_pages 96 | ]; 97 | } 98 | -------------------------------------------------------------------------------- /api/requests/occurrences.php: -------------------------------------------------------------------------------- 1 | query($count_sql); 82 | return $stmt->fetchColumn(); 83 | } 84 | 85 | function fetch_occurrences($results_per_page, $offset, $filters = [], $columns = [], $joined_columns = []) { 86 | global $pdo; 87 | 88 | list($joinClauses, $selectClause) = build_join_and_select_clauses($filters, $columns, $joined_columns); 89 | $whereClauses = build_where_clauses($filters); 90 | 91 | $sql = " 92 | SELECT 93 | $selectClause 94 | FROM 95 | occurrences o 96 | $joinClauses 97 | $whereClauses 98 | ORDER BY 99 | o.occurrence_status ASC 100 | LIMIT $results_per_page OFFSET $offset 101 | "; 102 | 103 | $stmt = $pdo->query($sql); 104 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 105 | } 106 | 107 | function get_results($results_per_page, $offset, $filters = [], $columns = [], $joined_columns = []) { 108 | $total_occurrences = count_total_occurrences($filters, $columns, $joined_columns); 109 | $occurrences = fetch_occurrences($results_per_page, $offset, $filters, $columns, $joined_columns); 110 | $total_pages = ceil($total_occurrences / $results_per_page); 111 | 112 | return [ 113 | 'occurrences' => $occurrences, 114 | 'totalPages' => $total_pages 115 | ]; 116 | } 117 | -------------------------------------------------------------------------------- /api/requests/pages.php: -------------------------------------------------------------------------------- 1 | query($count_sql); 57 | return $stmt->fetchColumn(); 58 | } 59 | 60 | function fetch_pages($results_per_page, $offset, $filters = []) { 61 | global $pdo; 62 | 63 | $whereClauses = build_where_clauses($filters); 64 | $joinClauses = build_join_clauses($filters); 65 | 66 | $sql = " 67 | SELECT 68 | p.page_id, 69 | p.page_url, 70 | SUM(CASE WHEN o.occurrence_status = 'active' THEN 1 ELSE 0 END) AS page_occurrences_active 71 | FROM 72 | pages p 73 | INNER JOIN 74 | occurrences o ON p.page_id = o.occurrence_page_id 75 | $joinClauses 76 | $whereClauses 77 | GROUP BY p.page_id 78 | ORDER BY page_occurrences_active DESC 79 | LIMIT $results_per_page OFFSET $offset 80 | "; 81 | 82 | $stmt = $pdo->query($sql); 83 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 84 | } 85 | 86 | function get_results($results_per_page, $offset, $filters = []) { 87 | $total_pages = count_total_pages($filters); 88 | $pages = fetch_pages($results_per_page, $offset, $filters); 89 | $total_pages_count = ceil($total_pages / $results_per_page); 90 | 91 | return [ 92 | 'pages' => $pages, 93 | 'totalPages' => $total_pages_count 94 | ]; 95 | } 96 | -------------------------------------------------------------------------------- /api/requests/properties.php: -------------------------------------------------------------------------------- 1 | query($count_sql); 57 | return $stmt->fetchColumn(); 58 | } 59 | 60 | function fetch_properties($results_per_page, $offset, $filters = []) { 61 | global $pdo; 62 | 63 | $joinClauses = build_join_clauses($filters); 64 | $whereClauses = build_where_clauses($filters); 65 | 66 | $sql = " 67 | SELECT 68 | p.property_id, 69 | p.property_name 70 | FROM 71 | properties p 72 | INNER JOIN 73 | occurrences o ON p.property_id = o.occurrence_property_id 74 | $joinClauses 75 | $whereClauses 76 | GROUP BY p.property_id 77 | LIMIT $results_per_page OFFSET $offset 78 | "; 79 | 80 | $stmt = $pdo->query($sql); 81 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 82 | } 83 | 84 | function get_results($results_per_page, $offset, $filters = []) { 85 | $total_properties = count_total_properties($filters); 86 | $properties = fetch_properties($results_per_page, $offset, $filters); 87 | $total_pages = ceil($total_properties / $results_per_page); 88 | 89 | return [ 90 | 'properties' => $properties, 91 | 'totalPages' => $total_pages 92 | ]; 93 | } 94 | -------------------------------------------------------------------------------- /api/requests/queued_scans.php: -------------------------------------------------------------------------------- 1 | $scans, 13 | 'totalScans' => $total_scans, 14 | 'totalPages' => $total_pages 15 | ]; 16 | 17 | return $response; 18 | 19 | } -------------------------------------------------------------------------------- /api/requests/statuses.php: -------------------------------------------------------------------------------- 1 | prepare($sql); // Use prepare instead of query when using variables 48 | $stmt->execute(); 49 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 50 | } 51 | 52 | function get_results($results_per_page = '', $offset = '', $filters = []) { 53 | $statusCounts = fetch_statuses($filters); 54 | 55 | $statuses = []; 56 | foreach ($statusCounts as $row) { 57 | $statuses[$row['occurrence_status']] = (int)$row['count']; 58 | } 59 | 60 | return [ 61 | 'statuses' => $statuses 62 | ]; 63 | } 64 | -------------------------------------------------------------------------------- /api/requests/tags.php: -------------------------------------------------------------------------------- 1 | query($count_sql); 43 | return $stmt->fetchColumn(); 44 | } 45 | 46 | function fetch_tags($results_per_page, $offset, $filters = []) { 47 | global $pdo; 48 | 49 | $whereClauses = build_where_clauses_for_tags($filters); 50 | $sql = " 51 | SELECT 52 | t.tag_id, 53 | t.tag_name, 54 | COUNT(tr.occurrence_id) AS tag_reference_count 55 | FROM 56 | tags t 57 | INNER JOIN 58 | tag_relationships tr ON t.tag_id = tr.tag_id 59 | INNER JOIN 60 | occurrences o ON tr.occurrence_id = o.occurrence_id 61 | $whereClauses 62 | GROUP BY t.tag_id 63 | ORDER BY tag_reference_count DESC, t.tag_id 64 | LIMIT $results_per_page OFFSET $offset 65 | "; 66 | 67 | $stmt = $pdo->query($sql); 68 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 69 | } 70 | 71 | function get_results( $results_per_page, $offset, $filters = []) { 72 | $total_tags = count_total_tags($filters); 73 | $tags = fetch_tags($results_per_page, $offset, $filters); 74 | $total_pages = ceil($total_tags / $results_per_page); 75 | 76 | return [ 77 | 'tags' => $tags, 78 | 'totalPages' => $total_pages 79 | ]; 80 | } 81 | -------------------------------------------------------------------------------- /components/active_class.php: -------------------------------------------------------------------------------- 1 | 8 | 9 |
No active filters.
10 | 11 | 14 | 15 | 46 | 47 | -------------------------------------------------------------------------------- /components/chart.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

Status Occurrences Over Time

9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Month
24 |
25 | 108 |
109 | 110 | 113 | -------------------------------------------------------------------------------- /components/message_list.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |

Messages

12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
MessagesEqualified CountActive CountTotal Count
27 |
28 |
29 | 30 |
31 |
32 | 33 | 130 | 131 | -------------------------------------------------------------------------------- /components/message_occurrences_list.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |

Occurrences of Message

10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
Code SnippetPageStatus
22 |
23 |
24 | 25 |
26 |
27 | 28 | 113 | 114 | -------------------------------------------------------------------------------- /components/page_list.php: -------------------------------------------------------------------------------- 1 | 7 |
8 |

Pages

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
URLActive Occurrences Count
22 |
23 |
24 | 25 |
26 |
27 | 28 | 118 | 119 | -------------------------------------------------------------------------------- /components/page_occurrences_list.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |

Messages on Page

10 |
11 |
12 | 13 | 14 | 15 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | 29 |
16 | Message 17 | 19 | Code Snippet 20 | 22 | Status 23 |
30 |
31 |
32 | 33 |
34 |
35 | 36 | 125 | 126 | 129 | -------------------------------------------------------------------------------- /components/report_filter_search.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 11 | 35 | 36 | 145 | 146 | 149 | -------------------------------------------------------------------------------- /components/report_header.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | 20 | 21 | 34 | 35 | 39 | 40 |
41 | 42 | 46 | 47 |
48 |

49 | 50 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | 71 | 72 | (Linked to Main Report) 73 | 74 | 75 | 79 | 80 | 81 |

82 | 87 |
88 |
89 |
90 | 91 | ' . $errorMsg . ''; 10 | } 11 | 12 | // Success message 13 | if (isset($_SESSION['success'])) { 14 | $successMsg = isset($_GET['success']) ? $_GET['success'] : $_SESSION['success']; 15 | echo ''; 16 | } 17 | 18 | // Clear the session messages so they don't show again on refresh 19 | if (isset($_SESSION['success'])) { 20 | unset($_SESSION['success']); 21 | } 22 | if (isset($_SESSION['error'])) { 23 | unset($_SESSION['error']); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /components/tag_list.php: -------------------------------------------------------------------------------- 1 | 7 |
8 |

Tags

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
TagOccurrences Count
22 |
23 |
24 | 25 |
26 |
27 | 28 | 121 | 122 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "twbs/bootstrap": "^5.3.3", 4 | "alphagov/accessible-autocomplete": "2.0.4", 5 | "php": "^8.1.1", 6 | "vlucas/phpdotenv": "^5.6", 7 | "auth0/auth0-php": "^8.10", 8 | "nyholm/psr7": "^1.8", 9 | "guzzlehttp/guzzle": "^7.5", 10 | "http-interop/http-factory-guzzle": "^1.2" 11 | }, 12 | "config": { 13 | "allow-plugins": { 14 | "php-http/discovery": true 15 | } 16 | }, 17 | "scripts": { 18 | "post-install-cmd": [ 19 | "mkdir -p assets/bootstrap && cp -R vendor/twbs/bootstrap/dist assets/bootstrap", 20 | "mkdir -p assets/accessible-autocomplete && cp -R vendor/alphagov/accessible-autocomplete/dist assets/accessible-autocomplete" 21 | ], 22 | "post-update-cmd": [ 23 | "mkdir -p assets/bootstrap && cp -R vendor/twbs/bootstrap/dist assets/bootstrap", 24 | "mkdir -p assets/accessible-autocomplete && cp -R vendor/alphagov/accessible-autocomplete/dist assets/accessible-autocomplete" 25 | ] 26 | }, 27 | "repositories": [ 28 | { 29 | "type": "package", 30 | "package": { 31 | "name": "alphagov/accessible-autocomplete", 32 | "version": "2.0.4", 33 | "dist": { 34 | "url": "https://github.com/alphagov/accessible-autocomplete/archive/refs/tags/v2.0.4.zip", 35 | "type": "zip" 36 | }, 37 | "source": { 38 | "url": "https://github.com/alphagov/accessible-autocomplete", 39 | "type": "git", 40 | "reference": "releases/tag/v2.0.4/" 41 | }, 42 | "autoload": { 43 | "classmap": ["dist/"] 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /helpers/get_content.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT * FROM $table WHERE $id_column = :id"); 16 | $stmt->execute(['id' => $id]); 17 | $content = $stmt->fetchObject() ?: 'Content Not Found'; 18 | 19 | return $content; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /helpers/get_next_scannable_property.php: -------------------------------------------------------------------------------- 1 | prepare($query); 22 | $stmt->execute(); 23 | return $stmt->fetch(PDO::FETCH_ASSOC); 24 | } -------------------------------------------------------------------------------- /helpers/get_page.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EqualifyEverything/equalify/ae98f028bac07027cfde51171c2a0f15197056ba/helpers/get_page.php -------------------------------------------------------------------------------- /helpers/get_page_title.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT report_title FROM reports WHERE report_id = :report_id"); 22 | $stmt->execute(['report_id' => $report_id]); 23 | $report_title = $stmt->fetchColumn() ?: 'Report Not Found'; 24 | } 25 | if (isset($_GET['property_id'])) { 26 | $stmt = $pdo->prepare("SELECT property_name FROM properties WHERE property_id = :property_id"); 27 | $stmt->execute(['property_id' => $_GET['property_id']]); 28 | $property_name = $stmt->fetchColumn() ?: 'Property Not Found'; 29 | } 30 | if (isset($_GET['message_id'])) { 31 | $stmt = $pdo->prepare("SELECT message_title FROM messages WHERE message_id = :message_id"); 32 | $stmt->execute(['message_id' => $_GET['message_id']]); 33 | $message_title = $stmt->fetchColumn() ?: 'Message Not Found'; 34 | } 35 | if (isset($_GET['tag_id'])) { 36 | $stmt = $pdo->prepare("SELECT tag_name FROM tags WHERE tag_id = :tag_id"); 37 | $stmt->execute(['tag_id' => $_GET['tag_id']]); 38 | $tag_name = $stmt->fetchColumn() ?: 'Tag Not Found'; 39 | } 40 | if (isset($_GET['page_id'])) { 41 | $stmt = $pdo->prepare("SELECT page_url FROM pages WHERE page_id = :page_id"); 42 | $stmt->execute(['page_id' => $_GET['page_id']]); 43 | $page_url = $stmt->fetchColumn() ?: 'Page Not Found'; 44 | } 45 | 46 | switch ($view) { 47 | case 'property_settings': 48 | $title = ($property_name ? "$property_name" : "Untitled Property") . " Settings | Equalify"; 49 | break; 50 | case 'reports': 51 | $title = " All Reports | Equalify"; 52 | break; 53 | case 'scans': 54 | $title = "Scans | Equalify"; 55 | break; 56 | case 'settings': 57 | $title = "Settings | Equalify"; 58 | break; 59 | case 'account': 60 | $title = "My Account | Equalify"; 61 | break; 62 | case 'report': 63 | $title = ($report_title ? $report_title : "") . " Report | Equalify"; 64 | break; 65 | case 'message': 66 | $title = ($message_title ? $message_title : "") . " Message Detail | " . ($report_title ? $report_title : "") . " Report | Equalify"; 67 | break; 68 | case 'tag': 69 | $title = ($tag_name ? $tag_name : "") . " Tag Detail | Equalify " . ($report_title ? $report_title : "") . " Report | Equalify";; 70 | break; 71 | case 'page': 72 | $title = ($page_url ? $page_url : "") . " Page | " . ($report_title ? $report_title : "") . " Report | Equalify"; 73 | break; 74 | // Add other cases as needed 75 | } 76 | } 77 | 78 | return $title; 79 | } 80 | 81 | function the_active_page() 82 | { 83 | return isset($_GET['view']) ? $_GET['view'] : 'default'; 84 | } 85 | -------------------------------------------------------------------------------- /helpers/get_properties.php: -------------------------------------------------------------------------------- 1 | query($sql); 9 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /helpers/get_property.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 10 | 11 | // Bind the property_id parameter 12 | $stmt->bindParam(':property_id', $property_id, PDO::PARAM_INT); 13 | 14 | // Execute the statement 15 | $stmt->execute(); 16 | 17 | // Fetch and return the property 18 | return $stmt->fetch(PDO::FETCH_ASSOC); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /helpers/get_report_filters.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT report_filters FROM reports WHERE report_id = :report_id"); 9 | $stmt->execute(['report_id' => $report_id]); 10 | $db_raw_filters = $stmt->fetchColumn(); 11 | $db_raw_filters = $db_raw_filters ? json_decode($db_raw_filters, true) : []; 12 | $db_filters = []; 13 | foreach ($db_raw_filters as $filter) { 14 | $db_filters[] = $filter; 15 | } 16 | 17 | // Cookie Name 18 | $cookie_name = 'queue_report_' . $report_id . '_filter_change'; 19 | 20 | // Fetch filters from cookie 21 | $cookie_filters = isset($_COOKIE[$cookie_name]) ? json_decode($_COOKIE[$cookie_name], true) : []; 22 | 23 | // Merged cookie filters and DB filters 24 | $merged_filters = array_merge($db_filters, $cookie_filters); 25 | 26 | // Build a map of the latest filter_change directives for each unique filter 27 | $latest_directives = []; 28 | foreach ($merged_filters as $item) { 29 | if (isset($item['filter_change'])) { 30 | $key = $item['filter_type'] . '_' . $item['filter_id'] . '_' . $item['filter_value']; 31 | $latest_directives[$key] = $item['filter_change']; 32 | } 33 | } 34 | 35 | // Apply the latest directives 36 | $result_as_array = []; 37 | foreach ($merged_filters as $item) { 38 | $key = $item['filter_type'] . '_' . $item['filter_id'] . '_' . $item['filter_value']; 39 | if (!isset($latest_directives[$key]) || $latest_directives[$key] === 'add') { 40 | unset($item['filter_change']); 41 | $result_as_array[] = $item; 42 | } 43 | } 44 | 45 | // Grouping for string representation 46 | $grouped = []; 47 | foreach ($result_as_array as $item) { 48 | $grouped[$item['filter_type']][] = $item['filter_id']; 49 | } 50 | 51 | // Building the query string 52 | $query_string_parts = []; 53 | foreach ($grouped as $type => $ids) { 54 | $query_string_parts[] = $type . '=' . implode(',', $ids); 55 | } 56 | $result_as_string = implode('&', $query_string_parts); 57 | 58 | // Return results 59 | return array( 60 | 'as_string' => $result_as_string, 61 | 'as_array' => $result_as_array 62 | ); 63 | } 64 | ?> -------------------------------------------------------------------------------- /helpers/get_reports.php: -------------------------------------------------------------------------------- 1 | query($sql); 9 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /helpers/get_scans.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 30 | $stmt->execute(); 31 | return $stmt->fetchAll(PDO::FETCH_ASSOC); 32 | } -------------------------------------------------------------------------------- /helpers/get_scans_count.php: -------------------------------------------------------------------------------- 1 | query($sql); 8 | return $stmt->fetchColumn(); 9 | } -------------------------------------------------------------------------------- /helpers/get_title.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT report_title FROM reports WHERE report_id = :report_id"); 9 | $stmt->execute(['report_id' => $id]); 10 | $title = $stmt->fetchColumn() ?: 'Report Not Found'; 11 | 12 | } elseif($view == 'message'){ 13 | 14 | // Query to fetch message title 15 | $stmt = $pdo->prepare("SELECT message_title FROM messages WHERE message_id = :message_id"); 16 | $stmt->execute(['message_id' => $id]); 17 | $title = $stmt->fetchColumn() ?: 'Message Not Found'; 18 | 19 | } elseif($view == 'page'){ 20 | 21 | // Query to fetch page title 22 | $stmt = $pdo->prepare("SELECT page_url FROM pages WHERE page_id = :page_id"); 23 | $stmt->execute(['page_id' => $id]); 24 | $title = $stmt->fetchColumn() ?: 'Page Not Found'; 25 | 26 | } elseif($view == 'tag'){ 27 | 28 | // Query to fetch page title 29 | $stmt = $pdo->prepare("SELECT tag_name FROM tags WHERE tag_id = :tag_id"); 30 | $stmt->execute(['tag_id' => $id]); 31 | $title = $stmt->fetchColumn() ?: 'Page Not Found'; 32 | 33 | } 34 | 35 | return $title; 36 | } 37 | -------------------------------------------------------------------------------- /helpers/is_page_scanning.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 11 | $stmt->bindParam(':page_id', $page_id, PDO::PARAM_INT); 12 | $stmt->execute(); 13 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 14 | 15 | if($row){ 16 | return true; 17 | }else{ 18 | return false; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <?php echo get_page_title(); ?> 24 | 25 | 26 | 27 | 28 | 29 | Skip to main content 30 |
31 |
32 | 33 | Equalify Logo 34 | 35 | 56 |
57 |
58 |
59 | 60 | 70 | 71 |
72 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | safeLoad(); 11 | 12 | $GLOBALS["managed_mode"] = false; 13 | if (array_key_exists('MODE', $_ENV) && $_ENV['MODE'] == 'managed') { 14 | $GLOBALS["managed_mode"] = true; 15 | }; 16 | 17 | if($GLOBALS["managed_mode"]){ // if we're in managed mode, initialize auth0 18 | 19 | define('ROUTE_URL_INDEX', rtrim($_ENV['AUTH0_BASE_URL'], '/')); 20 | define('ROUTE_URL_LOGIN', ROUTE_URL_INDEX . '/?auth=login'); 21 | define('ROUTE_URL_CALLBACK', ROUTE_URL_INDEX . '/?auth=auth_callback'); 22 | define('ROUTE_URL_LOGOUT', ROUTE_URL_INDEX . '/?auth=logout'); 23 | 24 | $auth0 = new \Auth0\SDK\Auth0([ 25 | 'domain' => $_ENV['AUTH0_DOMAIN'], 26 | 'clientId' => $_ENV['AUTH0_CLIENT_ID'], 27 | 'clientSecret' => $_ENV['AUTH0_CLIENT_SECRET'], 28 | 'cookieSecret' => $_ENV['AUTH0_COOKIE_SECRET'] 29 | ]); 30 | 31 | $session = $auth0->getCredentials(); 32 | 33 | if (!empty($_GET['auth'])){ // Router for auth endpoints 34 | require_once 'actions/'.$_GET['auth'].'.php'; 35 | } 36 | 37 | if ($session === null) { // The user isn't logged in. 38 | require_once 'actions/login.php'; 39 | } else { 40 | $GLOBALS["ACTIVE_DB"] = $session->user['equalify_databases'][0]; // TODO: currently just takes first from DBs array, should be switchable 41 | } 42 | 43 | } 44 | 45 | // Database creds 46 | $db_host = $_ENV['DB_HOST']; 47 | $db_port = $_ENV['DB_PORT']; 48 | $db_name = $_ENV['DB_NAME']; 49 | $db_user = $_ENV['DB_USERNAME']; 50 | $db_pass = $_ENV['DB_PASSWORD']; 51 | 52 | // Set Current DB 53 | if($GLOBALS["managed_mode"]){ 54 | $current_db = $GLOBALS["ACTIVE_DB"]; 55 | }else{ 56 | $current_db = $_ENV['DB_NAME']; 57 | } 58 | 59 | // Create DB connection 60 | $pdo = new PDO("mysql:host=$db_host;port=$db_port;dbname=$current_db", "$db_user", "$db_pass"); 61 | $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 62 | 63 | // Start session 64 | session_start(); -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | Equalify Logo 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /theme.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: var(--bs-secondary-bg); 3 | } 4 | #reports_content{ 5 | min-height: 60vh; 6 | } 7 | .bi{ 8 | vertical-align: -.125em; 9 | pointer-events: none; 10 | fill: currentColor; 11 | } 12 | .me-2{ 13 | margin-right: .5rem !important; 14 | } 15 | a.active{ 16 | background-color: var(--bs-secondary-color) !important; 17 | } 18 | pre code{ 19 | white-space: break-spaces; 20 | word-break: break-word !important; 21 | } 22 | header .nav-link{ 23 | color: #212529; 24 | 25 | } 26 | header .nav-item { 27 | font-size: var(--bs-navbar-brand-font-size); 28 | } 29 | footer .nav-link{ 30 | text-decoration: underline; 31 | } 32 | footer nav li:before{ 33 | content: '|'; 34 | padding: 0 4px 0 10px; 35 | } 36 | footer nav li:first-of-type:before{ 37 | content: ' '; 38 | padding: 0; 39 | } 40 | .fs-7{ 41 | font-size: .9em; 42 | } 43 | #brand{ 44 | text-transform: uppercase; 45 | letter-spacing: .035em; 46 | } 47 | .badge .spinner-border-sm{ 48 | --bs-spinner-width: .7rem; 49 | --bs-spinner-height: 0.7rem; 50 | } 51 | #archived{ 52 | min-height: 1.5em; 53 | min-width: 2.5em; 54 | } 55 | .snippet-input{ 56 | width:100%; 57 | } 58 | .snippet-delete{ 59 | margin-right: 1em; 60 | } 61 | #reports_filter { 62 | overflow: hidden; 63 | } 64 | #reports_filter a{ 65 | width: 200px; 66 | position: relative; 67 | background: transparent !important; 68 | } 69 | @media (max-width: 768px) { 70 | #reports_filter a{ 71 | width: auto; 72 | } 73 | } 74 | #reports_filter a:before{ 75 | position: absolute; 76 | content: ''; 77 | bottom: -2em; 78 | left: 0; 79 | width: 100%; 80 | height: 1em; 81 | border-radius: 6px; 82 | transition: bottom .4s; 83 | } 84 | #reports_filter a.active:before{ 85 | bottom: -1em; 86 | } 87 | #reports_filter a:hover:before{ 88 | bottom: -1.25em; 89 | } 90 | #reports_filter a#equalified:before{ 91 | background: green; 92 | } 93 | #reports_filter a#active:before{ 94 | background: red; 95 | } 96 | #reports_filter a#ignored:before{ 97 | background: gray; 98 | } 99 | .btn-outline-secondary:hover code{ 100 | color: white !important; 101 | } 102 | a.row{ 103 | text-decoration: none; 104 | } 105 | a.row:hover{ 106 | background-color: var(--bs-secondary-bg) 107 | } -------------------------------------------------------------------------------- /views/account.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Equalify Account

4 |
5 | Logout 6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 32 |
33 |
34 |
35 | 38 | 41 |
42 |
43 |
44 |
45 | 62 | 63 | -------------------------------------------------------------------------------- /views/message.php: -------------------------------------------------------------------------------- 1 | '; 33 | } 34 | 35 | // The content 36 | $the_content = get_content('messages', $message_id) 37 | ?> 38 | 39 |
40 |
41 |

42 | 43 | message_title; 46 | ?> 47 | 48 |

49 |
50 | message_link)){ 53 | ?> 54 | 55 | 56 | More Info 57 | 58 | Link opens in new tab 59 | 60 | 64 | 65 | 66 | 69 | 70 |
71 |
72 | 73 | 80 | 81 |
-------------------------------------------------------------------------------- /views/page.php: -------------------------------------------------------------------------------- 1 | page_url; 42 | $page_property_id = $the_content->page_property_id; 43 | 44 | // Optional Report Header 45 | if(!empty($report_id)){ 46 | the_report_header(); 47 | }else{ 48 | // Add some space before the content 49 | echo '
'; 50 | } 51 | ?> 52 | 53 |
54 |
55 |

56 | Page: 57 | 61 | 62 |

63 |
64 | 65 | Send to Scan'; 69 | }else{ 70 | 71 | // Session data is required to scan page. 72 | $page_data = array( 73 | 'property_id' => $page_property_id, 74 | 'page_id' => $page_id, 75 | 'page_url' => $page_url 76 | ); 77 | if($report_id) // Report IDs can be blank 78 | $page_data['report_id'] = $report_id; 79 | $_SESSION['process_this_page'] = $page_data; 80 | 81 | echo 'Send to Scan'; 82 | } 83 | ?> 84 | 85 |
86 |
87 | 88 | 95 | 96 |
-------------------------------------------------------------------------------- /views/property_settings.php: -------------------------------------------------------------------------------- 1 | format('n/j/y \a\t G:i'); 21 | }else{ 22 | $scanned_date = 'Not processed'; 23 | } 24 | 25 | 26 | // Default data for new properties 27 | }else{ 28 | 29 | // Default data for new properties 30 | $property_id =''; 31 | $name = ''; 32 | $url = ''; 33 | $scanned_date = ''; 34 | $scanning = ''; 35 | 36 | } 37 | 38 | // Let's turn the ID into a session variable so 39 | // we can safely save existing content with the form. 40 | $_SESSION['property_id'] = $property_id; 41 | 42 | ?> 43 | 44 |
45 | 46 | 50 | 51 |

Settings

52 |
53 |
54 |

Scan Settings

55 |
56 | . 57 | 58 |
59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
Sitemaps must follow valid XML Sitemap schema.
Example:"https://equalify.app/sitemap.xml"
70 |
71 |
72 |
73 | 76 | 77 | Delete Property'; 81 | ?> 82 | 83 |
84 |
85 |
86 |
87 | 104 | -------------------------------------------------------------------------------- /views/report.php: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 |

Report Details

28 | 29 | 37 | 38 |
39 |
40 | 41 | 45 | 46 |
47 |
48 | 49 | 53 | 54 |
55 |
56 |
-------------------------------------------------------------------------------- /views/report_settings.php: -------------------------------------------------------------------------------- 1 | Report not found.

'; 21 | exit; 22 | } 23 | 24 | // Components 25 | require_once('components/report_filter_search.php'); 26 | require_once('components/active_filters.php'); 27 | 28 | $report_filters = get_report_filters(); 29 | 30 | // Use Session to securely handle report ID 31 | $_SESSION['report_id'] = $report_id; 32 | 33 | ?> 34 | 35 |
36 | 37 | 41 | 42 |

43 | 44 | 45 | 49 | 50 | 51 |

52 |

Report Settings

53 |
54 |
55 |

General Settings

56 |
57 | 58 | 67 | 68 |
69 |
70 |
71 |
72 |

Filters

73 |
74 | 75 | 82 | 83 |
84 | 85 | 90 | 91 | 103 | 106 | 107 |
108 |
109 |

Danger Zone

110 |

111 | Delete Report 112 |

113 |
114 |
115 |
116 | -------------------------------------------------------------------------------- /views/reports.php: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 15 | 16 |
17 |

Reports

18 |
19 | New Report 20 |
21 |
22 |
23 | 24 | 30 | 31 |
32 |
33 |
34 |

35 | 36 |

37 | 38 | View Report 39 | 40 |
41 |
42 |
43 | 44 |

No reports.

New to Equalify?

Checkout this video to get started.
'; 48 | endif; 49 | ?> 50 | 51 |
52 | -------------------------------------------------------------------------------- /views/scans.php: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 | 30 | 31 |

Scans

32 |
33 | 34 |
35 |
36 |
37 |

Scan Queue

38 |
39 | 40 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
Job IdPage URLPropertyActions
No scans queued.
78 | 81 |
90 | 129 |
130 |
131 | -------------------------------------------------------------------------------- /views/settings.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 | 16 | 17 |

Equalify Settings

18 |
19 |

Properties

20 |
21 | 22 | 27 | 28 |
29 |
30 |
31 | 32 |

33 | 37 |

38 | 39 | 40 | 44 | Edit Property 45 | 46 |
47 | 48 | format('n/j/y \a\t G:i'); 53 | echo 'Processed '.$formatted_date.'.'; 54 | } 55 | ?> 56 |
57 |
58 | 59 | 63 | 64 |
65 |

66 | Add Property 67 |

68 |
69 |
-------------------------------------------------------------------------------- /views/tag.php: -------------------------------------------------------------------------------- 1 | '; 34 | } 35 | ?> 36 | 37 |
38 |

39 | 40 | 44 | 45 |

46 | 47 | 54 | 55 |
--------------------------------------------------------------------------------