├── .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 |
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:
';
368 | foreach ($logged_messages as $message){
369 | $success_message.= "- $message
";
370 | }
371 | $success_message.= "
";
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 |
16 |
17 | $filter_type, 'filter_value' => $filter_value, 'filter_id' => $filter_id, 'filter_change' => 'remove']);
26 | $query = "report_id=$report_id&filters[]=" . urlencode($filter_string);
27 | ?>
28 |
29 | -
30 |
31 | :
32 |
33 |
34 |
37 | Remove Filter
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/components/chart.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
Status Occurrences Over Time
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Month |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
108 |
109 |
110 |
113 |
--------------------------------------------------------------------------------
/components/message_list.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
Messages
12 |
13 |
14 |
15 |
16 |
17 | Messages |
18 | Equalified Count |
19 | Active Count |
20 | Total Count |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
130 |
131 |
--------------------------------------------------------------------------------
/components/message_occurrences_list.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
Occurrences of Message
10 |
11 |
12 |
13 |
14 |
15 | Code Snippet |
16 | Page |
17 | Status |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
113 |
114 |
--------------------------------------------------------------------------------
/components/page_list.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
Pages
9 |
10 |
11 |
12 |
13 |
14 | URL |
15 | Active Occurrences Count |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
118 |
119 |
--------------------------------------------------------------------------------
/components/page_occurrences_list.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
Messages on Page
10 |
11 |
12 |
13 |
14 |
15 |
16 | Message
17 | |
18 |
19 | Code Snippet
20 | |
21 |
22 | Status
23 | |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
125 |
126 |
129 |
--------------------------------------------------------------------------------
/components/report_filter_search.php:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
145 |
146 |
149 |
--------------------------------------------------------------------------------
/components/report_header.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
20 |
21 |
34 |
35 |
39 |
40 |
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 '' . $successMsg . '
';
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 | Tag |
15 | Occurrences Count |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
24 |
25 |
26 |
27 |
28 |
29 | Skip to main content
30 |
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 |
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 |
7 |
8 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
Deleting an account will remove all data associated with the account. You cannot undo a deletion.
54 |
55 |
59 |
60 |
61 |
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 |
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 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
Deleting a property will remove all data associated with the property. You cannot undo a deletion.
96 |
97 |
101 |
102 |
103 |
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 |
52 |
Report Settings
53 |
54 |
71 |
72 |
Filters
73 |
74 |
75 |
82 |
83 |
84 |
85 |
90 |
91 |
103 |
106 |
107 |
108 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/views/reports.php:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
15 |
16 |
22 |
23 |
24 |
30 |
31 |
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 | Job Id |
56 | Page URL |
57 | Property |
58 | Actions |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | No scans queued. |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | |
75 | |
76 | |
77 |
78 |
81 | |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
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 |
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 |
--------------------------------------------------------------------------------